# TileMap

A MapLibre/PMTiles vector map (in a GraphicBlock) with configurable centre, zoom, projection and style; provides context for child layers and callouts.

**Category:** Components/Graphics/TileMap

**Import:** `import { TileMap } from '@reuters-graphics/graphics-components'`

## Props

| Prop | Type | Default | Required | Description |
|------|------|---------|:--------:|-------------|
| `title` | `string \| Snippet` | — |  | Title of the map as a string or a custom snippet. |
| `description` | `string` | — |  | Description of the map, passed in as a markdown string. |
| `notes` | `string \| Snippet` | — |  | Notes to the map, passed in as a markdown string or a custom snippet. |
| `id` | `string` | ``map-${Math.random().toString(36).substring(2, 9)}`` |  | Map container id |
| `center` | `[number, number]` | `[0, 0]` |  | Map center coordinates [longitude, latitude] |
| `zoom` | `number` | `2` |  | Initial zoom level |
| `minZoom` | `number` | `0` |  | Minimum zoom level |
| `maxZoom` | `number` | `22` |  | Maximum zoom level |
| `projection` | `ProjectionSpecification` | — |  | Map projection. Use 'globe' for 3D globe view. See https://maplibre.org/maplibre-style-spec/types/#projectiondefinition |
| `interactive` | `boolean` | `true` |  | Enable interactive controls (zoom, pan, etc.) |
| `styleUrl` | `string` | `'https://graphics.thomsonreuters.com/reuters-protomaps/style.json'` |  | Map style URL |
| `emphasizeLabels` | `boolean` | `false` |  | Darken the basemap's place labels and give them a strong white halo, so city/region names stay readable over colored data layers. Applied once on map load; a no-op on styles without `place` labels (e.g. non-default `styleUrl`s), so it degrades gracefully. |
| `height` | `string` | `'500px'` |  | Map height (default: '500px') |
| `width` | `ContainerWidth` | `'normal'` |  | Width of the map within the text well. Options: `normal`, `wide`, `wider`, `widest`, `fluid` |
| `textWidth` | `ContainerWidth` | `'normal'` |  | Set a different width for the text within the text well, for example, "normal" to keep the title, description and notes inline with the rest of the text well. Can't ever be wider than `width`. |
| `onMapReady` | `(map: maplibregl.Map) => void` | — |  | Callback function that receives the map instance when ready |
| `legend` | `Snippet` | — |  | Optional content rendered inside the map's `GraphicBlock`, directly above the map. Use it to share a single block — with one title and the standard spacing — between the map and an accompanying element such as a `Legend`. |
| `children` | `Snippet` | — |  | Child components (e.g., TileMapLayer) |

## Examples

### Demo

```svelte
<TileMap id="demo-map" center={[-98, 39]} zoom={3} interactive={true} height="500px" />
```

### Globe view

```svelte
<TileMap
  id="globe-map"
  center={[0, 0]}
  zoom={2}
  projection={{ type: 'globe' }}
  interactive={true}
  title="Globe View"
  description="A 3D globe projection of the Earth."
  height="600px"
/>
```

### Non-interactive

```svelte
<TileMap
  id="static-map"
  center={[2.3522, 48.8566]}
  zoom={12}
  interactive={false}
  title="Paris"
  description="A static map of Paris, France."
  height="400px"
/>
```

### With GeoJSON layers

```svelte
<TileMap
  id="geojson-map"
  center={[-73.9712, 40.7831]}
  zoom={11}
  interactive={true}
  title="Map with GeoJSON Layers"
  description="Example showing multiple GeoJSON layers including local data and data fetched from a URL."
  notes="Demonstrates polygon fills, line overlays, point markers, and text labels."
  height="600px"
>
  <TileMapLayer
    id="park-area"
    data={centralParkData}
    type="fill"
    paint={{
      'fill-color': '#179639',
      'fill-opacity': 0.7,
    }}
  />
  <TileMapLayer
    id="park-outline"
    data={centralParkData}
    type="line"
    paint={{
      'line-color': '#228b22',
      'line-width': 2,
    }}
  />
  <TileMapLayer
    id="route-line"
    data="https://raw.githubusercontent.com/datanews/ny-marathon/refs/heads/master/route.geojson"
    type="line"
    paint={{
      'line-color': '#4287f5',
      'line-width': 2,
    }}
  />
  <TileMapLayer
    id="reuters-office-point"
    data={reutersOfficePoint}
    type="circle"
    paint={{
      'circle-radius': 4,
      'circle-color': '#ff0000',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    }}
  />
  <TileMapLayer
    id="reuters-office-label"
    data={reutersOfficePoint}
    type="symbol"
    layout={{
      'text-field': 'Reuters Office',
      'text-offset': [0.25, 0.5],
      'text-anchor': 'top-left',
      'text-size': 12,
    }}
    paint={{
      'text-color': '#000000',
      'text-halo-color': '#ffffff',
      'text-halo-width': 2,
    }}
  />
  <TileMapLayer
    id="points-layer"
    data={points}
    type="circle"
    paint={{
      'circle-radius': 4,
      'circle-color': '#9c27b0',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    }}
  />
</TileMap>
```

### Labels above data

```svelte
<TileMap
  id="labels-map"
  center={[-93.5, 42]}
  zoom={6}
  interactive={true}
  emphasizeLabels
  title="Keeping labels readable over data"
  description="`emphasizeLabels` (on the map) darkens the basemap's place labels and adds a white halo; `beneathLabels` (on the fill layer) inserts the data below the labels, so city names across Iowa stay on top of the colored overlay."
  height="500px"
>
  <TileMapLayer
    id="iowa-fill"
    data={iowaData}
    type="fill"
    beneathLabels
    paint={{
      'fill-color': '#d64000',
      'fill-opacity': 0.75,
    }}
  />
</TileMap>
```

### With callouts

```svelte
<TileMap
  id="callout-map"
  center={[-73.9868, 40.7567]}
  zoom={13}
  interactive={true}
  title="Map with coordinate callouts"
  description="TileMapCallout can be rendered directly inside TileMap and manages the MapLibre marker lifecycle for the coordinate."
  notes="The examples show above, below and flipped placement so labels can avoid important map features or viewport edges."
  height="500px"
>
  <TileMapCallout lngLat={[-73.9868, 40.7567]}>Times Square</TileMapCallout>
  <TileMapCallout lngLat={[-73.9776, 40.7527]} placement="above" flip>
    Grand Central
  </TileMapCallout>
  <TileMapCallout lngLat={[-73.9942, 40.7505]} placement="below">
    Penn Station
  </TileMapCallout>
</TileMap>
```

### With Geocoder

```svelte
<TileMap
  id="geocoder-map"
  center={[-98, 39]}
  zoom={3}
  interactive={true}
  title="Map with Geocoder"
  description="Search for a location to fly the map there."
  height="500px"
  onMapReady={(map) => {
    geocoderMapRef = map;
  }}
>
  <div
    style="position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 1000; width: min(500px, calc(100% - 20px));"
  >
    <Geocoder
      accessToken={mapboxAccessToken}
      onselect={(loc) => {
        geocoderMapRef?.flyTo({ center: [loc.lng, loc.lat], zoom: 10 });
      }}
    />
  </div>
</TileMap>
```

## Documentation

# TileMap

Easily add an interactive map to your page using MapLibre GL and PMTiles.

```svelte
<script>
  import { TileMap } from '@reuters-graphics/graphics-components';
</script>

<TileMap
  id="my-map"
  center={[-74.006, 40.7128]}
  zoom={10}
  interactive={true}
  height="500px"
/>
```

The TileMap component uses [MapLibre GL JS](https://maplibre.org/) and [PMTiles](https://protomaps.com/docs/pmtiles) for efficient, interactive mapping. It automatically configures the PMTiles protocol and uses the Reuters Protomaps style by default.

## Globe view

Use the `projection` prop to display a 3D globe. The projection accepts a [ProjectionSpecification](https://maplibre.org/maplibre-style-spec/types/#projectiondefinition) object:

```svelte
<TileMap
  id="globe-map"
  center={[0, 0]}
  zoom={1}
  projection={{ type: 'globe' }}
  interactive={true}
  height="600px"
/>
```

## Non-interactive mode

Disable interaction for static maps:

## Adding GeoJSON layers

Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass GeoJSON data directly or fetch it from a URL. Layer rendering order will directly correspond to the order in which you add the layers in the code.

> **Note for TypeScript users:** When passing GeoJSON data objects, you'll need to type cast them using `as GeoJSON` to ensure TypeScript recognizes the correct type. This provides better type safety and error messages. See examples below.

### Basic example with local data

```svelte
<script lang="ts">
  import { TileMap, TileMapLayer } from '@reuters-graphics/graphics-components';
  import type { GeoJSON } from 'geojson';

  const parkData = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Polygon',
          coordinates: [
            [
              [-73.9577, 40.8005],
              [-73.9816, 40.7684],
              [-73.973, 40.7649],
              [-73.9492, 40.7969],
              [-73.9577, 40.8005],
            ],
          ],
        },
        properties: { name: 'Central Park' },
      },
    ],
  };
</script>

<TileMap center={[-73.9712, 40.7831]} zoom={13}>
  <!-- Polygon fill -->
  <TileMapLayer
    id="park-fill"
    data={parkData as GeoJSON}
    type="fill"
    paint={{
      'fill-color': '#179639',
      'fill-opacity': 0.7,
    }}
  />

  <!-- Polygon outline -->
  <TileMapLayer
    id="park-outline"
    data={parkData as GeoJSON}
    type="line"
    paint={{
      'line-color': '#228b22',
      'line-width': 2,
    }}
  />
</TileMap>
```

### Fetching GeoJSON from a URL

You can also pass a URL string to fetch GeoJSON data:

```svelte
<TileMap center={[-73.9712, 40.7831]} zoom={11}>
  <TileMapLayer
    id="marathon-route"
    data="https://example.com/path/to/route.geojson"
    type="line"
    paint={{
      'line-color': '#4287f5',
      'line-width': 2,
    }}
  />
</TileMap>
```

### Adding point markers

```svelte
<script lang="ts">
  import type { GeoJSON } from 'geojson';

  const officeLocation = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [-73.9868, 40.7567],
        },
        properties: { name: 'Office' },
      },
    ],
  };
</script>

<TileMap center={[-73.9868, 40.7567]} zoom={14}>
  <!-- Point marker -->
  <TileMapLayer
    id="office-point"
    data={officeLocation as GeoJSON}
    type="circle"
    paint={{
      'circle-radius': 6,
      'circle-color': '#ff0000',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    }}
  />

  <!-- Text label -->
  <TileMapLayer
    id="office-label"
    data={officeLocation as GeoJSON}
    type="symbol"
    layout={{
      'text-field': 'Office',
      'text-offset': [0, 1],
      'text-anchor': 'top',
      'text-size': 12,
    }}
    paint={{
      'text-color': '#000000',
      'text-halo-color': '#ffffff',
      'text-halo-width': 2,
    }}
  />
</TileMap>
```

### TileMapLayer Props

- **id**: `string` (required) - Unique identifier for the layer
- **data**: `object | string` (required) - GeoJSON data or URL to fetch from
- **type**: `'fill' | 'line' | 'circle' | 'symbol' | 'fill-extrusion' | 'raster' | 'background' | 'heatmap' | 'hillshade'` - Layer type (default: `'fill'`)
- **paint**: `Record<string, unknown>` - Paint properties for the layer
- **layout**: `Record<string, unknown>` - Layout properties for the layer
- **beforeId**: `string` - Layer ID to insert before (for ordering)
- **beneathLabels**: `boolean` - Insert the layer beneath the basemap's labels so place names stay readable on top of it (default: `false`). Ignored when `beforeId` is set. A no-op on styles with no symbol layers.
- **minZoom**: `number` - Minimum zoom level to display layer
- **maxZoom**: `number` - Maximum zoom level to display layer
- **filter**: `unknown[]` - Filter expression for the layer

### Multiple layers example

You can combine multiple layers to create rich visualizations:

```svelte
<TileMap center={[-73.9712, 40.7831]} zoom={11}>
  <!-- Background polygon -->
  <TileMapLayer
    id="area-fill"
    data={areaData}
    type="fill"
    paint={{ 'fill-color': '#179639', 'fill-opacity': 0.7 }}
  />

  <!-- Border line -->
  <TileMapLayer
    id="area-border"
    data={areaData}
    type="line"
    paint={{ 'line-color': '#228b22', 'line-width': 2 }}
  />

  <!-- Route from URL -->
  <TileMapLayer
    id="route"
    data="https://example.com/route.geojson"
    type="line"
    paint={{ 'line-color': '#4287f5', 'line-width': 2 }}
  />

  <!-- Point markers -->
  <TileMapLayer
    id="markers"
    data={pointsData}
    type="circle"
    paint={{
      'circle-radius': 4,
      'circle-color': '#9c27b0',
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    }}
  />
</TileMap>
```

## Adding coordinate callouts

Use `TileMapCallout` when you want a framed annotation attached to a map coordinate without writing MapLibre marker glue yourself. Render it as a child of `TileMap` and pass a longitude/latitude pair.

```svelte
<script lang="ts">
  import {
    TileMap,
    TileMapCallout,
  } from '@reuters-graphics/graphics-components';
</script>

<TileMap center={[-73.9868, 40.7567]} zoom={13}>
  <TileMapCallout lngLat={[-73.9868, 40.7567]}>Times Square</TileMapCallout>

  <TileMapCallout
    lngLat={{ longitude: -73.9776, latitude: 40.7527 }}
    placement="below"
    flip
  >
    Bryant Park
  </TileMapCallout>
</TileMap>
```

`placement="above" | "below"` moves the callout surface above or below the coordinate, while `flip` moves the surface to the left of the coordinate. Use these together to keep labels clear of important map features and viewport edges.

### TileMapCallout Props

- **lngLat**: `[longitude, latitude] | { lng, lat } | { lon, lat } | { longitude, latitude }` (required) - Coordinate to attach the callout to
- **placement**: `'above' | 'below'` - Place the callout surface above or below the coordinate (default: `'above'`)
- **flip**: `boolean` - Flip the callout surface to the left of the coordinate (default: `false`)
- **class**: `string` - Add custom classes to the callout surface
- **id**: `string` - Add an id to the MapLibre marker wrapper
- **children**: `Snippet` - Short label or compact rich content

## Advanced usage

For more control over the map, you can use the `onMapReady` callback to access the MapLibre GL instance:

```svelte
<script>
  import { TileMap } from '@reuters-graphics/graphics-components';
  import type { Map as MaplibreMap } from 'maplibre-gl';

  function handleMapReady(map: MaplibreMap) {
    // Add custom layers, sources, or event handlers
    map.addSource('my-source', {
      type: 'geojson',
      data: myGeoJsonData,
    });

    map.addLayer({
      id: 'my-layer',
      type: 'circle',
      source: 'my-source',
      paint: {
        'circle-radius': 8,
        'circle-color': '#007cbf',
      },
    });
  }
</script>

<Map id="custom-map" center={[0, 0]} zoom={2} onMapReady={handleMapReady} />
```

## Props

- **center**: `[longitude, latitude]` - Map center coordinates (default: `[0, 0]`)
- **zoom**: `number` - Initial zoom level (default: `2`)
- **minZoom**: `number` - Minimum zoom level (default: `0`)
- **maxZoom**: `number` - Maximum zoom level (default: `22`)
- **interactive**: `boolean` - Enable interactive controls (default: `true`)
- **styleUrl**: `string` - Map style URL (default: Reuters Protomaps style)
- **emphasizeLabels**: `boolean` - Darken the basemap's place labels and add a white halo so city/region names stay readable over colored data layers (default: `false`). Applied on map load; a no-op on styles without `place` labels.
- **height**: `string` - Map height in CSS units (default: `'500px'`)
- **width**: `ContainerWidth` - Width within the text well (default: `'normal'`)
- **onMapReady**: `(map: maplibregl.Map) => void` - Callback when map is ready
