# Xatra: The Matplotlib of Maps

Xatra is the matplotlib of maps. You can create historical maps (static or dynamic, i.e. with a time slider), data maps, maps of administrative regions, whatever. Example maps produced with `xatra` can be seen [here](https://srajma.github.io/maps/).

![](examples/nations_indosphere.png)

![](examples/sea.png)

![](examples/suvarnabhumi.png)

![](examples/admin_india_3.png)


## Installation

```bash
pip install xatra
xatra-install-data
```

Or if installing to a local virtual environment:

```bash
uv add xatra
source .venv/bin/activate
xatra-install-data
```

This installs `xatra`, then downloads GADM administrative boundaries, Natural Earth rivers, and other geographical data from [Hugging Face](https://huggingface.co/datasets/srajma/xatra-data) to `~/.xatra/data/`.

[Old alpha version of xatra](https://github.com/srajma/xatra1). Always make sure you're using version 2.11 or later.

## Example: Historical map

```python
import xatra
from xatra.loaders import gadm, naturalearth
from xatra.territory_library import NORTH_INDIA

map = xatra.Map()

# Flags automatically get colors from the default LinearColorSequence
# Flags with the same label will use the same color
map.Flag(label="Maurya", value=gadm("IND") | gadm("PAK"))
map.Flag(label="Chola", value=gadm("IND.31") | gadm("IND.17") - gadm("IND.17.5"))
map.Flag(label="Gupta", value=NORTH_INDIA)
map.River(label="Ganga", value=naturalearth("1159122643"))
map.Path(label="Uttarapatha", value=[[28,77],[30,90],[40, 120]], note="Ancient northern trade route")
map.Point(label="Indraprastha", position=[28,77], note="Ancient capital of the Pandavas")
map.Text(label="Jambudvipa", position=[22,79], note="Ancient name for the Indian subcontinent")
map.TitleBox("<b>Sample historical map of India</b><br>Classical period, source: Majumdar.")
map.show()
```

## Example: A map with everything (except dataframes)

And here's a more complex example, of a dynamic map (items can have periods so they only show up at certain time periods), with base tile layers, notes that show up in tooltips, and custom CSS for each object:

```python
import xatra
from xatra.loaders import gadm, naturalearth
from xatra.territory_library import NORTH_INDIA

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.BaseOption("Esri.WorldImagery")
map.BaseOption("OpenTopoMap")
map.BaseOption("Esri.WorldPhysical")
map.Flag(label="Maurya", value=gadm("IND") | gadm("PAK"), period=[-320, -240], note="south is lost after Ashoka's death")
map.Flag(label="Maurya", value=NORTH_INDIA, period=[-320, -180])
map.Flag(label="Gupta", value=NORTH_INDIA, period=[250, 500])
map.Flag(label="Chola", value=gadm("IND.31"), note="Chola persisted throughout this entire period")
map.Admin(gadm="IND.31", level=3)
map.AdminRivers(sources=["naturalearth", "overpass"], classes="all-rivers", note="All rivers from Natural Earth and Overpass data")
map.River(label="Ganga", value=naturalearth("1159122643"), note="can be specified as naturalearth(id) or overpass(id, osm_type=None|'relation'|'way')", classes="ganga-river indian-river")
map.River(label="Ganga", value=naturalearth("1159122643"), period=[0, 600], note="Modern course of Ganga")
map.Path(label="Uttarapatha", value=[[28,77],[30,90],[40, 120]], classes="uttarapatha-path")
map.Path(label="Silk Road", value=[[35.0, 75.0], [40.0, 80.0], [45.0, 85.0]], period=[-200, 600])
map.Point(label="Indraprastha", position=[28,77])
map.Point(label="Delhi", position=[28.6, 77.2], period=[400, 800])
map.Text(label="Jambudvipa", position=[22,79], classes="jambudvipa-text", note="Ancient name for the Indian subcontinent")
map.Text(label="Aryavarta", position=[22,79], period=[0, 600], note="Land of the Aryans")
map.TitleBox("<b>Map of major Indian empires</b><br>Classical period, source: Majumdar.")
map.TitleBox("<h2>Ancient Period (-500 to 0)</h2><p>This title appears only in ancient times</p>", period=[-500, 0])
map.TitleBox("<h2>Classical Period (-100 to 400)</h2><p>This title appears only in classical times</p>", period=[-100, 400])
map.slider(-480, 700) 
map.CSS("""
/* applies to all elements of given class */
.flag { stroke: #555; fill: rgba(200,0,0,0.4); }
.river { stroke: #0066cc; stroke-width: 2; }
.path { stroke: #8B4513; stroke-width: 2; stroke-dasharray: 5 5;}
#title { background: rgba(255,255,255,0.95); border: 1px solid #ccc; padding: 12px 16px; border-radius: 8px; max-width: 360px; z-index: 1000; }
.flag-label { color: #888;}
#controls, #controls input {width:90%;}

/* Specific styling for individual elements */
.indian-river { stroke: #ff0000; }
.ganga-river { stroke-width: 4; }
.uttarapatha-path { stroke: #ff0000; stroke-width: 2; stroke-dasharray: 5 5; }
.jambudvipa-text { font-size: 24px; font-weight: normal; color: #666666; }
.chola-tehsils { stroke: #8B4513; stroke-width: 0.5; }
.all-rivers { stroke-width: 3; opacity: 0.7; }
""")

map.show()
```

## Example: Administrative map

Here's a taluk-level administrative map of the Indian subcontinent

```python
import xatra

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.BaseOption("Esri.WorldImagery")
map.BaseOption("OpenTopoMap")
map.BaseOption("Esri.WorldPhysical")
map.Admin(gadm="IND", level=3)
map.Admin(gadm="PAK", level=3) # level-3 GADM divisions in Pak are more like districts, but we don't have finer data
map.Admin(gadm="BGD", level=3)
map.Admin(gadm="AFG", level=2) # level-2 is the best we have for Afghanistan
map.Admin(gadm="NPL", level=3) # level-3 GADM divisions in Nepal are more like districts, but level-4 is WAY too fine
map.Admin(gadm="BTN", level=2) # level-2 is the best we have for Bhutan, and they're like taluks anyway
map.Admin(gadm="LKA", level=2) # level-2 is the best we have for Lanka, and they're like taluks anyway
map.AdminRivers(sources=["naturalearth", "overpass"])
map.TitleBox("<b>Taluk-level map of the Indian subcontinent.")
map.show()
```

## Example: Dataframe map

And here's a data map using DataFrames:

```python
import pandas as pd
import xatra
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.BaseOption("Esri.WorldImagery")
map.BaseOption("OpenTopoMap")
map.BaseOption("Esri.WorldPhysical")

### STATIC MAP
df = pd.DataFrame({
    'GID': ['IND.31', 'IND.12', 'IND.20', 'Z01.14'],
    'population': [100, 200, 150, 100],
    # '2021': [100, 200, 150, 100],
    'note': ['ooga', 'booga', 'kooga', 'mooga'] # optional, shows up in tooltips
})

df.set_index('GID', inplace=True)
map.DataColormap(plt.cm.viridis, norm=LogNorm())
map.Dataframe(df)

map.show()
```

Or a dynamic map with a time slider: 

```python
import pandas as pd
import xatra
import matplotlib.pyplot as plt
map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.BaseOption("Esri.WorldImagery")
map.BaseOption("OpenTopoMap")
map.BaseOption("Esri.WorldPhysical")

### DYNAMIC MAP
df = pd.DataFrame({
    'GID': ['IND.31', 'IND.12', 'Z01.14'],
    '2020': [100, 200, 100],
    '2020_note': ['2020_ooga', '2020_booga', '2020_mooga'],
    '2021': [110, 210, 110],
    '2021_note': ['2021_ooga', '2021_booga', '2021_mooga'],
    '2022': [120, 220, 340]
})

df.set_index('GID', inplace=True)
map.DataColormap(norm=LogNorm())
map.Dataframe(df)

map.show()
```

## Tip on coloring historical maps well

Successive Flags you define are assigned colors based on the map's `FlagColorSequence`, which is a class that determines how the next color is calculated. The default color sequence increments successive colors' hues by the conjugate golden ratio of the hue spectrum (taken from 0 to 1, so 0 is red, 1/3 is green, 2/3 is blue and 1 is red again) mod 1:

```python
map.FlagColorSequence(LinearColorSequence(colors=[<random>], step = Color.hsl(GOLDEN_RATIO, 0.0, 0.0)))
```

This is [best for making nearby colors as contrasting as possible](https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/) -- so as a general tip: place nearby flags near each other.

Sometimes you want to group some flags to be "similarly-colored" -- e.g. nations allied with each other, or belonging to the same religion. You can do this by assigning different color sequences to different groups, and using a smaller step size for each group's color sequence:

```python
#!/usr/bin/env python3

import xatra
from xatra.loaders import gadm, naturalearth
from xatra.colorseq import LinearColorSequence, Color
from matplotlib.colors import LinearSegmentedColormap

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.BaseOption("Esri.WorldImagery")
map.BaseOption("OpenTopoMap")
map.BaseOption("Esri.WorldPhysical")

map.FlagColorSequence(LinearColorSequence(colors=[Color.hex("#ff0000")], step=Color.hsl(0.03, 0.0, 0.0)), class_name="hindu")
map.FlagColorSequence(LinearColorSequence(colors=[Color.hex("#00ff00")], step=Color.hsl(0.03, 0.0, 0.0)), class_name="muslim")

map.Flag(label="Vijayanagara", value=gadm("IND.16") | gadm("IND.2") | gadm("IND.32") | gadm("IND.31") | gadm("IND.17"), classes="hindu")
map.Flag(label="Yadava", value=gadm("IND.20"), classes="hindu")
map.Flag(label="Rajput", value=gadm("IND.29"), classes="hindu")
map.Flag(label="Mughal", value=gadm("IND.25") | gadm("IND.12") | gadm("IND.34"), classes="muslim")
map.Flag(label="Ahmedabad", value=gadm("IND.11"), classes="muslim")
map.Flag(label="Bhopal", value=gadm("IND.19"), classes="muslim")
map.Flag(label="Gajapati", value=gadm("IND.26"), classes="hindu")

map.show(out_json="map_colorgroups.json", out_html="map_colorgroups.html")
```
## Disputed Territories

Some stuff to note about disputed territories:
- these are coded not under their country name (e.g. "IND.12") but under some `Z<some number>`. E.g. Jammu and Kashmir is Z01.14.
- they are typically contained under the json files of the countries that administer them. So e.g. Z01 (Jammu and Kashmir) is in the India data while Z06 (Pakistan-occupied Jammu and Gilgit-Baltistan) is in the Pakistan data.
- **This is the only important thing you need to know:** you cannot write `gadm("Z01")` -- there are no zeroth-level disputed territories, because those would just be the countries they belong to. You *can* do `gadm("Z01.14")`, `gadm("Z01.14.1")` etc. There are also custom territories `Z01`, `Z02` etc. that you can import from `xatra.territory_library`.

When mapping a country, e.g. `map.Admin(gadm="IND", level=1)`, it will map all the regions administered by it (i.e. contained in its file). When mapping a specific disputed region, e.g. `map.Flag(label="Kashmir", value=gadm("Z01.14"))` xatra finds out from data/disputed_territories/disputed_mapping.json which country it belongs to.

If for whatever reason that doesn't work, you can also specify which countries' files to find it in, e.g. `map.Admin(gadm="Z01.14", level=3, find_in_gadm=["IND"])`. But you probably won't need to use it.

The `find_in_gadm` parameter is available for:
- `map.Admin()` - Administrative regions
- `map.Flag()` - When using `gadm()` loader
- `map.Dataframe()` - DataFrame elements

## Directly doing `xatra.Flag() etc.`

All of the elements of `Map` e.g. `Flag()`, `River()` can also be directly called as methods of `xatra` i.e. `xatra.Flag()` etc. and it will apply to the "current plot". This is useful for modularity, i.e. creating some map as a module that can simply be imported into another map.

```python
#!/usr/bin/env python3
"""
Example demonstrating pyplot-style interface for Xatra.

This example shows how to use xatra.Flag(), xatra.River(), etc. 
directly without explicitly creating a Map object, similar to
how matplotlib.pyplot works.
"""

import xatra
from xatra.loaders import gadm, naturalearth
from xatra.territory_library import NORTH_INDIA

# No need to create a map object - just start adding elements!
# A Map is automatically created on first use.

xatra.BaseOption("Esri.WorldTopoMap", default=True)
xatra.BaseOption("Esri.WorldImagery")
xatra.Flag(label="Maurya", value=gadm("IND") | gadm("PAK"), period=[-320, -240])
xatra.Flag(label="Maurya", value=NORTH_INDIA, period=[-320, -180])
xatra.Flag(label="Gupta", value=NORTH_INDIA, period=[250, 500])
xatra.Flag(label="Chola", value=gadm("IND.31"), note="Chola persisted throughout")
xatra.River(label="Ganga", value=naturalearth("1159122643"), classes="major-river")
xatra.Path(label="Uttarapatha", value=[[28, 77], [30, 90], [40, 120]])
xatra.Point(label="Indraprastha", position=[28, 77])
xatra.Text(label="Jambudvipa", position=[22, 79], note="Ancient name for the Indian subcontinent")
xatra.TitleBox("<b>Pyplot-style Map Example</b><br>Classical period, using xatra.Flag() etc.")
xatra.CSS("""
.flag { stroke: #555; fill: rgba(200,0,0,0.4); }
.major-river { stroke: #0066cc; stroke-width: 3; }
""")

xatra.slider(-320, 500)

xatra.show(out_json="tests/map_pyplot.json", out_html="tests/map_pyplot.html")
print("Map exported to tests/map_pyplot.html")
```

## `xatrahub()` imports

`xatrahub(path, filter_only=None, filter_not=None)` loads artifacts from a XatraHub API (default `http://localhost:8088`, override with `XATRAHUB_URL`).

- Path formats accepted by the hub API:
  - `/{kind}/{name}` or `/{kind}/{name}/{version}`
  - `/{username}/{kind}/{name}` or `/{username}/{kind}/{name}/{version}` (legacy format)
- `kind="lib"`: returns a namespace object, so use `lib = xatrahub("...")` then `lib.TERRITORY_NAME`.
- `kind="map"` or `kind="css"`: applies imported code and returns `None`.
- `filter_only` / `filter_not` (lists of method names like `["Flag", "River"]`) filter imported `xatra.<Method>(...)` calls for map/css imports.

```python
import xatra
from xatra import xatrahub

indic = xatrahub("/lib/dtl/alpha")  # lib import returns namespace
xatrahub("/map/mauryan_extent/3", filter_not=["TitleBox"])  # map import applies into current map
```

## Search

Two search boxes appear in the **TitleBox** (the draggable panel at the top-left):

1. **Search map features** — Finds elements you drew on the map (flags, points, paths, rivers, text labels). Focus the box to see a dropdown of all features; typing filters by **label** and **note** (e.g. searching “capital” will match a point whose note says “Capital of India”). Selecting a result pans the map to that feature.
2. **Search place** — Geocoder to find places worldwide (addresses, cities, etc.). By default uses **Nominatim** (OpenStreetMap); no API key needed. For higher usage or other providers, call `map.Geocoder(provider, api_key)` before `map.show()` — e.g. `map.Geocoder("mapbox", api_key="pk.xxx")` or `map.Geocoder("google", api_key="...")`. Supported providers: `nominatim`, `mapbox`, `google`, `photon`.

```python
map = xatra.Map()
# … add flags, points, etc. …
map.Geocoder()   # optional: use Nominatim (default)
# map.Geocoder("mapbox", api_key="pk.xxx")
map.show()
```

## API Reference

### Map

The main class for creating maps.

```python
map = Map()
```

#### Methods

##### Adding Map Elements

The most important element of a Map is a "Flag". A Flag is a country or kingdom, and defined by a label, a territory (consisting of some algebra of GADM regions) and optionally a "period" (if period is left as None then the flag is considered to be active for the whole period of time).

- **`Flag(label, territory=None, period=None, note=None, color=None, classes=None, type=None, inherit=None)`**: Add a flag (country/kingdom). Use `type="vassal"` or `type="province"` for slash-separated hierarchical labels like `India/Karnataka`. Use `inherit="OtherFlag"` to force color inheritance from an earlier-defined flag.
- **`Dataframe(dataframe, data_column=None, year_columns=None, classes=None)`**: Add DataFrame-based choropleth data
- **`Admin(gadm, level, period=None, classes=None, color_by_level=1)`**: Add administrative regions from GADM data
- **`AdminRivers(period=None, classes=None, sources=None)`**: Add rivers from specified data sources
- **`River(label, geometry, note=None, classes=None, period=None, show_label=False, n_labels=1, hover_radius=10)`**: Add a river with optional label display and customizable hover detection radius
- **`Path(label, coords, note=None, classes=None, period=None, show_label=False, n_labels=1, hover_radius=10)`**: Add a path/route with optional tooltip note, label display, and customizable hover detection radius
- **`Point(label, position, note=None, period=None, icon=None, show_label=False, hover_radius=20, classes=None)`**: Add a point of interest with optional tooltip note, custom icon, label display, customizable hover detection radius, and CSS classes
- **`Text(label, position, note=None, classes=None, period=None)`**: Add a text label with optional tooltip note
- **`TitleBox(html, period=None)`**: Add a title box with HTML content
- **`Geocoder(provider="nominatim", api_key=None)`**: Set the place-search (geocoder) provider. Use `nominatim` (default), `mapbox`, `google`, or `photon`. API key required for mapbox/google.
- **Music(path: str, timestamps: tuple = None, period: tuple = None)**: Add an audio track to the map: if timestamps is set then only that segment of the audio track; if period is set then only to that segment of the map.

##### Styling and Configuration

- **`CSS(css)`**: Add custom CSS styles
- **`BaseOption(url_or_provider, name=None, default=False)`**: Add base map layer. `url_or_provider` may be either a raw Leaflet tile URL template or one of the built-in providers: `OpenStreetMap`, `OpenTopoMap`, `Esri.WorldImagery`, `Esri.WorldPhysical`, `Esri.WorldTopoMap`, `Esri.WorldTerrain`, `Esri.WorldShadedRelief`, `Esri.OceanBasemap`, `CartoDB.Positron`, `CartoDB.PositronNoLabels`, `USGS.USImageryTopo`, `Stadia.StamenWatercolor`, or `Stadia.StamenTerrainBackground`. For Stadia providers and raw `stadiamaps.com` tile URLs, set `STADIA_API_KEY` in `.env`.
- **`FlagColorSequence(color_sequence, class_name=None)`**: Set the color sequence for flags
- **`AdminColorSequence(color_sequence)`**: Set the color sequence for admin regions
- **`DataColormap(colormap, vmin=None, vmax=None, norm=None)`**: Set the color map for data elements
- **`zoom(level: int)`**: Set the zoom level. Defaults to 5.
- **`focus(latitude: float, longitude: float)`**: set where the map is initially focused at. If not set, xatra auto-focuses to the center of the bounding box of rendered `Flag`/`Admin`/`Data`/`Dataframe` geometries. `River`/`Path`/`Point`/`Text` are only used for auto-focus when none of those four are present.
- **`slider(start=None, end=None, speed=5.0)`**: Set time limits and play speed for dynamic maps (speed in years per second)
- **`simplify(tolerance=None)`**: Use precomputed simplified GADM geometry for this map (`None` disables) [DON'T USE; IT WON'T HELP YOU]

##### Export

- **`show(out_json="map.json", out_html="map.html")`**: Export map to JSON and HTML files

### Pyplot-Style Functions

For convenience, Xatra provides pyplot-style functions that operate on a global "current map". These functions are available at the top level of the `xatra` module and mirror the `Map` methods.

#### Map Management

- **`get_current_map()`**: Get the current Map instance, creating one if none exists
- **`set_current_map(map)`**: Set the current Map instance (or None to clear)
- **`new_map()`**: Create a new Map and set it as current

#### Map methods are xatra methods

All Map methods are available as top-level functions that operate on the current map:

**Adding elements:**
- **`xatra.Flag(...)`**: Add a flag to the current map
- **`xatra.River(...)`**: Add a river to the current map
- **`xatra.Path(...)`**: Add a path to the current map
- **`xatra.Point(...)`**: Add a point to the current map
- **`xatra.Text(...)`**: Add a text label to the current map
- **`xatra.TitleBox(...)`**: Add a title box to the current map
- **`xatra.Geocoder(...)`**: Set geocoder provider for the current map
- **`xatra.Admin(...)`**: Add administrative regions to the current map
- **`xatra.AdminRivers(...)`**: Add rivers to the current map
- **`xatra.Dataframe(...)`**: Add DataFrame data to the current map

**Styling and Configuration**

- **`xatra.CSS(...)`**: Add CSS styles to the current map
- **`xatra.BaseOption(...)`**: Add a base map layer to the current map
- **`xatra.FlagColorSequence(...)`**: Set flag color sequence for the current map
- **`xatra.AdminColorSequence(...)`**: Set admin color sequence for the current map
- **`xatra.DataColormap(...)`**: Set data colormap for the current map
- **`xatra.slider(...)`**: Set time controls for the current map

**Export**

- **`xatra.show(...)`**: Export the current map to JSON and HTML files

**Note:** All parameters are identical to the corresponding Map methods. The functions simply call the method on the current map instance.

### CSS Classes and Styling

Xatra provides powerful CSS-based styling for all map elements. Each element type receives default CSS classes, and you can add custom classes for fine-grained control.

#### Default CSS Classes

Every element type automatically receives a default CSS class that you can use for styling:

| Element Type | Default Class | Element |
|--------------|---------------|---------|
| `Flag()` | `.flag` | The flag geometry (polygon/path) |
| Flag labels | `.flag-label` | The text label that appears at the flag's centroid |
| `River()` | `.river` | River line geometry |
| `Path()` | `.path` | Path line geometry |
| `Point()` | `.point` | Point marker |
| `Text()` | `.text` | Text label |
| `Admin()` | `.admin` | Administrative region geometry |
| `Dataframe()` | `.dataframe` | DataFrame visualization geometry |
| `TitleBox()` | `#title` | The title box container (ID, not class) |
| `slider()` | `#controls` | The time slider controls (ID, not class) |

#### Custom CSS Classes

You can add custom classes to most elements for individual styling. Custom classes are added using the `classes` parameter:

```python
# Add custom classes to flags
map.Flag(label="Maurya", value=gadm("IND"), classes="empire hindu")
map.Flag(label="Chola", value=gadm("IND.31"), classes="kingdom hindu")
map.Flag(label="Mughal", value=gadm("IND.25"), classes="empire muslim")

# Add custom classes to other elements
map.River(label="Ganga", value=naturalearth("1159122643"), classes="sacred-river major-river")
map.Path(label="Silk Road", value=[[35, 75], [40, 80]], classes="trade-route ancient")
map.Text(label="Jambudvipa", position=[22, 79], classes="region-name large-text", note="Ancient name for the Indian subcontinent")
map.Admin(gadm="IND.31", level=3, classes="tamil-nadu-tehsils")
# DataFrame elements can be styled with CSS classes
# map.Dataframe(df, classes="state-data")
```

**Important:** Flag labels automatically inherit the custom classes from their flag entry. This means if you define:

```python
map.Flag(label="Maurya", value=gadm("IND"), classes="empire hindu")
```

Both the flag geometry and its label will receive the classes `empire` and `hindu`, in addition to their default classes (`.flag` and `.flag-label` respectively).

#### Styling with CSS

Use the `CSS()` method to add custom styles. You can target default classes, custom classes, or both:

```python
map.CSS("""
/* Style all flags */
.flag {
  stroke: #333;
  stroke-width: 1;
  fill-opacity: 0.4;
}

/* Style all flag labels */
.flag-label {
  font-size: 14px;
  font-weight: bold;
  color: #666;
}

/* Style specific custom classes */
.empire {
  stroke-width: 3;
  stroke: #ff0000;
}

.kingdom {
  stroke-width: 1;
  stroke-dasharray: 5 5;
}

.hindu {
  fill: rgba(255, 200, 100, 0.4);
}

.muslim {
  fill: rgba(100, 200, 255, 0.4);
}

/* Style flag labels with custom classes */
.flag-label.empire {
  font-size: 18px;
  font-weight: bold;
}

.flag-label.kingdom {
  font-size: 14px;
  font-style: italic;
}

/* Style rivers */
.river {
  stroke: #0066cc;
  stroke-width: 2;
}

.sacred-river {
  stroke: #ff6600;
  stroke-width: 4;
}

/* Style paths */
.path {
  stroke: #8B4513;
  stroke-width: 2;
  stroke-dasharray: 5 5;
}

.trade-route {
  stroke: #FFD700;
  stroke-width: 3;
}

/* Style text labels */
.text {
  font-size: 16px;
  color: #666;
}

.region-name {
  font-size: 24px;
  font-weight: bold;
}

/* Style the title box */
#title {
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid #ccc;
  padding: 12px 16px;
  border-radius: 8px;
  max-width: 400px;
}

/* Style the time controls */
#controls {
  background: rgba(255, 255, 255, 0.9);
  padding: 10px;
  border-radius: 5px;
}
""")
```

### Territory Class

Represents geographical regions with set algebra operations.

#### Static Methods

- **`from_geojson(geojson_obj)`**: Create from GeoJSON object
- **`from_gadm(key)`**: Create from GADM administrative boundary
- **`from_naturalearth(ne_id)`**: Create from Natural Earth dataset
- **`from_polygon(coords, holes=None)`**: Create from custom polygon coordinates

#### Operations

- **`territory1 | territory2`**: Union of two territories
- **`territory1 - territory2`**: Difference of two territories
- **`territory1 & territory2`**: Intersection of two territories

### Data Loaders

- **`gadm(key, simplify_tolerance=None)`**: Load GADM administrative boundary (e.g., "IND", "PAK")
- **`naturalearth(ne_id)`**: Load Natural Earth feature by ID
- **`overpass(osm_id, osm_type=None)`**: Load Overpass data by OSM ID.
  Searches local cache first (`data/rivers_overpass_india`), then fetches from Overpass API if missing, normalizes to GeoJSON, saves locally, and returns it.
  `osm_type` can be `"relation"` or `"way"` to restrict lookup/fetch (including filename matching).
- **`polygon(coords, holes=None)`**: Create custom polygon territory from coordinates

### Custom Polygon Territories

The `polygon()` loader creates custom polygon territories that can be combined with other territories using set algebra operations. This is useful for defining arbitrary regions that don't correspond to existing administrative boundaries.

```python
from xatra.loaders import polygon, gadm

# Create a simple rectangular territory
rectangle = polygon([
    [25, 75],  # [latitude, longitude]
    [25, 85],
    [15, 85],
    [15, 75]
])

# Use set algebra with GADM regions
# Union: combine custom polygon with existing territory
extended_india = gadm("IND") | polygon([[30, 70], [35, 70], [35, 80], [30, 80]])

# Intersection: clip a territory to a bounding box
south_india = gadm("IND") & polygon([[8, 72], [8, 88], [20, 88], [20, 72]])

# Difference: remove custom area from territory
india_without_region = gadm("IND") - polygon([[25, 75], [25, 85], [30, 85], [30, 75]])

# Create polygon with holes (e.g., representing a lake or exclusion zone)
region_with_hole = polygon(
    [[20, 75], [25, 75], [25, 80], [20, 80]],  # Exterior ring
    holes=[[[21, 76], [24, 76], [24, 79], [21, 79]]]  # Interior hole
)

# Use in a map
map = xatra.Map()
map.Flag("Custom Region", extended_india)
map.Flag("Clipped Region", south_india, color="#ff0000")
map.show()
```

**Coordinate Format:**
- Coordinates are specified as `[latitude, longitude]` pairs (same as `Path()` and `Point()`)
- The polygon is automatically closed if the first and last coordinates differ
- Interior holes are specified as a list of coordinate rings

**Set Algebra Operations:**
- `|` (union): Combines two territories
- `-` (difference): Subtracts one territory from another
- `&` (intersection): Returns the overlapping area of two territories

### Color Sequences

Xatra supports automatic color assignment for both flags and admin regions using color sequences. By default, maps use `LinearColorSequence()` which generates contrasting colors. You can also use other color sequences:

- **`LinearColorSequence()`**: Generates contrasting colors (default)
- **`RotatingColorSequence()`**: Cycles through a predefined set of colors
- **`RandomColorSequence()`**: Generates random colors

#### Examples

```python
from xatra.colorseq import RotatingColorSequence, LinearColorSequence

# Set custom color sequences
map.FlagColorSequence(RotatingColorSequence())
map.AdminColorSequence(LinearColorSequence())

# Flags will automatically get colors from the sequence
map.Flag("Empire A", territory1)
map.Flag("Empire B", territory2)

# Admin regions will also get colors from their sequence
map.Admin(gadm="IND", level=3, color_by_level=1)

# You can also override with custom colors
map.Flag("Custom Empire", territory3, color="#ff0000")
```

#### Class-Based Color Sequences

Flags can be assigned to CSS classes for different color sequences. This allows you to group related flags (e.g., by religion, alliance, or historical period) with similar color schemes:

```python
from xatra.colorseq import RotatingColorSequence, LinearColorSequence

# Set up different color sequences for different classes
map.FlagColorSequence(LinearColorSequence(), None)  # Default sequence
map.FlagColorSequence(RotatingColorSequence(), "empire")  # For empires
map.FlagColorSequence(LinearColorSequence(), "kingdom")  # For kingdoms

# Add flags with different classes
map.Flag("Maurya", territory1, classes="empire")  # Uses empire colors
map.Flag("Gupta", territory2, classes="empire")   # Uses empire colors
map.Flag("Chola", territory3, classes="kingdom")  # Uses kingdom colors
map.Flag("Pandya", territory4, classes="kingdom") # Uses kingdom colors
map.Flag("Generic", territory5)  # Uses default colors (no classes)

# Flags with multiple classes use the first matching class
map.Flag("Mixed", territory6, classes="kingdom empire")  # Uses kingdom colors

# Flags with unknown classes fall back to default
map.Flag("Unknown", territory7, classes="unknown-class")  # Uses default colors
```

Flag labels automatically use a darker, more opaque version of the flag color for better readability.

### Vassals (Slash Labels)

Use slash-separated labels plus `type=` for hierarchical relationships:
- **Vassals**: `type="vassal"` (light HSLA overlay fill)
- **Provinces**: `type="province"` (transparent fill + thin root-colored border)
- **Multi-level hierarchies**: `India/Deccan/Bijapur`

```python
import xatra
from xatra.loaders import gadm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)

# Parent
map.Flag(label="India", value=gadm("IND"), note="Republic of India")
# Vassals
map.Flag(label="India/Karnataka", value=gadm("IND.16"), type="vassal")
map.Flag(label="India/Tamil Nadu", value=gadm("IND.31"), type="vassal")
# Provinces
map.Flag(label="India/Deccan", value=gadm("IND.2"), type="province")
map.Flag(label="India/Deccan/Bijapur", value=gadm("IND.16"), type="province")

map.show()
```

Vassal behavior:
- Parent/root-parent/depth are inferred from slash labels
- `type="vassal"`: fill uses `hsla(random_hue, 100%, 90%, 0.6)`
- `type="province"`: fill is transparent, border is thin with `opacity: 0.8` in root parent color
- Children are classed as `.vassal` or `.province` for custom CSS
- Child labels use leaf names (for example `Vidisa` for `Maurya/Avantirastra/Vidisa`)
- Child label color matches root parent, with size scaled by `0.85^depth`

### Data Visualization with DataFrames

Xatra provides powerful data visualization capabilities through the `Dataframe` method, which creates efficient choropleth maps from pandas DataFrames. This is the primary way to visualize data on maps.

The `Dataframe` method creates efficient choropleth maps from pandas DataFrames where each row represents an administrative division indexed by GID, and columns represent either a single data value or time-series data.

#### Static DataFrames (Single Data Column)

For static maps with a single data value per region:

```python
import pandas as pd
import xatra
import matplotlib.pyplot as plt

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)

# Create a static DataFrame
df = pd.DataFrame({
    'GID': ['IND.31', 'IND.12', 'IND.20', 'Z01.14'],
    'population': [1000000, 2000000, 1500000, 500000],
    'note': ['Coastal state', 'Major state', 'Growing region', 'Disputed territory']
})

df.set_index('GID', inplace=True)

# Use custom colormap
map.DataColormap(plt.cm.viridis, vmin=0, vmax=2000000)
map.Dataframe(df)

map.show()
```

#### Dynamic DataFrames (Time-Series Data)

For dynamic maps with time-series data:

```python
import pandas as pd
import xatra
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)

# Create a dynamic DataFrame with year columns
df = pd.DataFrame({
    'GID': ['IND.31', 'IND.12', 'IND.20'],
    '2020': [1000000, 2000000, 1500000],
    '2021': [1100000, 2100000, 1600000],
    '2022': [1200000, 2200000, 1700000],
    '2020_note': ['Base year', 'Major growth', 'Steady increase'],
    '2021_note': ['Continued growth', 'Peak growth', 'Accelerating'],
    '2022_note': ['Sustained growth', 'Plateau', 'Strong growth']
})

df.set_index('GID', inplace=True)

# Use logarithmic colormap for wide-ranging data
map.DataColormap(plt.cm.plasma, norm=LogNorm(vmin=1000000, vmax=2200000))
map.Dataframe(df)
map.slider(2020, 2022)

map.show()
```

#### DataFrame Structure and Requirements

**Required Structure:**
- Must be a pandas DataFrame
- Must have GID as index or as a column named 'GID'
- GID values must correspond to GADM administrative codes (e.g., 'IND.31', 'PAK.1', 'Z01.14')

**Static Maps:**
- Single data column containing numeric values
- Optional `note` column for tooltip information
- Auto-detected when DataFrame has exactly one data column

**Dynamic Maps:**
- Multiple columns with year names (e.g., '2020', '2021', '2022')
- Optional `(year)_note` columns for year-specific tooltip information
- Auto-detected when DataFrame has multiple numeric columns

**Note Columns:**
- **Static maps**: Use a column named `note` for general tooltip information
- **Dynamic maps**: Use columns named `(year)_note` (e.g., `2020_note`, `2021_note`) for year-specific tooltip information
- Note columns are automatically excluded from data visualization and map type detection

#### Parameters

- `dataframe`: pandas DataFrame with GID-indexed rows and data columns
- `data_column`: Column name containing the data values (for static maps). If None, auto-detected.
- `year_columns`: List of year columns for time-series data (for dynamic maps). If None, auto-detected.
- `classes`: Optional CSS classes for styling
- `find_in_gadm`: Optional list of country codes to search in if GID is not found in its own file

#### Features

- **Automatic map type detection**: Static vs dynamic based on DataFrame structure
- **Missing value handling**: NaN values are rendered as fully transparent (blank)
- **Rich tooltips**: Shows data values, notes, and administrative information
- **Efficient rendering**: Data processed once during export, not on every render
- **Color mapping**: Uses the map's DataColormap for visualization
- **Time support**: Dynamic maps work with time slider and period filtering
- **Memory optimization**: Shared geometry for multiple data points per region

#### DataColormap Configuration

The `DataColormap` method controls how data values are mapped to colors. It supports both linear and non-linear color mapping:

```python
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm, PowerNorm

# Default colormap (yellow-orange-red)
map.DataColormap()

# Matplotlib colormaps
map.DataColormap(plt.cm.viridis)      # Viridis colormap
map.DataColormap(plt.cm.plasma)       # Plasma colormap
map.DataColormap(plt.cm.Reds)         # Red colormap
map.DataColormap(plt.cm.Blues)        # Blue colormap

# Custom value range
map.DataColormap(plt.cm.viridis, vmin=0, vmax=1000)

# Logarithmic normalization (great for wide-ranging data)
map.DataColormap(plt.cm.viridis, norm=LogNorm())
map.DataColormap(plt.cm.plasma, norm=LogNorm(vmin=100, vmax=1000000))

# Power normalization (custom gamma)
map.DataColormap(plt.cm.viridis, norm=PowerNorm(gamma=0.5))

# Custom colormap from color list
from matplotlib.colors import LinearSegmentedColormap
custom_cmap = LinearSegmentedColormap.from_list("custom", ["#000000", "#ffffff"])
map.DataColormap(custom_cmap)
```

**Parameters:**
- `colormap`: matplotlib colormap (e.g., `plt.cm.viridis`, `plt.cm.Reds`)
- `vmin`: Minimum value for color mapping (optional, auto-detected from data)
- `vmax`: Maximum value for color mapping (optional, auto-detected from data)
- `norm`: Normalization object (e.g., `LogNorm()`, `PowerNorm(gamma=0.5)`)

**Features:**
- **Automatic value range detection**: If vmin/vmax not specified, uses data min/max
- **Non-linear normalization**: Support for LogNorm, PowerNorm, and other matplotlib Normalize objects
- **Color bar legend**: Automatically displays the colormap with value range
- **SVG color bar**: High-quality vector color bar for the legend

### Administrative Regions

The `Admin` method displays administrative regions directly from GADM data with automatic coloring and rich tooltips:

```python
# Show all tehsils in Tamil Nadu, colored by state
map.Admin(gadm="IND.31", level=3)

# Show all tehsils in India, colored by district
map.Admin(gadm="IND", level=3, color_by_level=2)

# Show districts colored by state with custom styling
map.Admin(gadm="IND", level=2, color_by_level=1, classes="districts")
```

**Parameters:**
- `gadm`: GADM key (e.g., "IND.31" for Tamil Nadu)
- `level`: Administrative level to display (0=country, 1=state, 2=district, 3=tehsil)
- `color_by_level`: Level to group colors by (default: 1 for states)
- `period`: Optional time period as [start_year, end_year] list
- `classes`: Optional CSS classes for styling

**Features:**
- **Rich tooltips**: Shows all GADM properties (GID_0, COUNTRY, GID_1, NAME_1, etc.) on hover
- **Automatic coloring**: Regions are colored by the specified administrative level
- **Boundary-aware matching**: Uses exact prefix matching to avoid false matches
- **Time support**: Works with dynamic maps and period filtering

**Color grouping examples:**
- `level=3, color_by_level=1`: Tehsils colored by state (all tehsils in same state have same color)
- `level=3, color_by_level=2`: Tehsils colored by district (all tehsils in same district have same color)
- `level=2, color_by_level=1`: Districts colored by state (all districts in same state have same color)

### Admin Rivers

The `AdminRivers` method displays all rivers from Natural Earth and Overpass data files with automatic source identification and rich tooltips:

```python
# Show all rivers from data files
map.AdminRivers()

# Show only Natural Earth rivers
map.AdminRivers(sources=["naturalearth"])

# Show only Overpass rivers
map.AdminRivers(sources=["overpass"])

# Show all rivers with custom styling
map.AdminRivers(classes="all-rivers")

# Show rivers for a specific time period
map.AdminRivers(period=[1800, 1900], classes="historical-rivers")
```

**Parameters:**
- `period`: Optional time period as [start_year, end_year] list
- `classes`: Optional CSS classes for styling
- `sources`: List of data sources to include (default: ["naturalearth", "overpass"])

**Features:**
- **Source filtering**: Choose which data sources to include (Natural Earth, Overpass, or both)
- **Source identification**: Rivers are colored differently by source (blue for Natural Earth, orange for Overpass)
- **Rich tooltips**: Shows source information, IDs, and all available name fields
- **Automatic loading**: Loads rivers from specified data sources
- **Overpass compatibility**: Accepts both cached GeoJSON and raw Overpass JSON (`elements`) files
- **Time support**: Works with dynamic maps and period filtering

**Tooltip information:**
- **Natural Earth rivers**: Shows "Natural Earth River", NE ID, and available name fields
- **Overpass rivers**: Shows "Overpass River", filename, and available name fields
- **Name fields**: Displays name, NAME, NAME_EN, NAME_LOC, NAME_ALT, NAME_OTHER as available
- **Additional properties**: Scale rank, feature class, min zoom level, etc.


### Point Icons

The `Icon` class provides four ways to create custom marker icons: external URLs, Bootstrap Icons, Leaflet marker built-ins, and geometric shapes.

Use `Icon.to_html()` to get the exact `<img>` element as rendered, handy for legends.

#### External URL Icons

Use custom icons from any web URL:

```python
import xatra
from xatra import Icon

# Custom icon from URL
custom_icon = Icon(
    icon_url="https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png",
    shadow_url="https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
    shadow_size=(41, 41)
)
map.Point(label="Custom Red Marker", position=[19.0, 73.0], icon=custom_icon)
```

#### Bootstrap Icons (Standard Library)

Use `Icon.bootstrap(...)` to reference the standard [Bootstrap Icons](https://icons.getbootstrap.com/) set from jsDelivr:

```python
from xatra import Icon

# Center-anchored POI icons
temple = Icon.bootstrap("bank2", icon_size=26)
map.Point(label="Temple", position=[13.0, 80.2], icon=temple)

port = Icon.bootstrap("anchor-fill", icon_size=24)
map.Point(label="Port", position=[19.0, 73.0], icon=port)

# Optional bottom-center anchor for pin-like placement
town = Icon.bootstrap("geo-alt-fill", icon_size=24, icon_anchor=(12, 24), popup_anchor=(0, -20))
map.Point(label="Town", position=[12.3, 76.6], icon=town)

# Optional self-hosted icon directory (avoids CDN dependency)
self_hosted = Icon.bootstrap("bank2", base_url="/static/bootstrap-icons")
map.Point(label="Offline-safe Temple", position=[11.0, 76.9], icon=self_hosted)
```

Note: Bootstrap icons are loaded from CDN at map-view time, so the viewer needs internet access.
If you pass `base_url`, icons are loaded from your own path (`<base_url>/<icon-name>.svg`) instead.

Defaults in `Icon.bootstrap(...)`:
- `icon_size=24`
- `icon_anchor=(width/2, height/2)` (center)
- `popup_anchor=(0, -height/2)`

These defaults avoid the common misalignment issue where non-pin icons are anchored at the marker tip.

#### Leaflet Marker Built-ins

**Leaflet Markers:**
- `marker-icon.png` - Default blue marker
- `marker-icon-red.png` - Red marker
- `marker-icon-green.png` - Green marker

```python
# Built-in Leaflet markers (with marker shadow/anchors preconfigured)
default_marker = Icon.builtin("marker-icon.png")
map.Point(label="Default Marker", position=[13.0, 80.2], icon=default_marker)

red_marker = Icon.builtin("marker-icon-red.png")
map.Point(label="Red Marker", position=[23.0, 72.6], icon=red_marker)
```

#### Geometric Shape Icons

Create lightweight geometric shape icons using pure SVG instead of image files. These shapes are generated dynamically and provide scalable, customizable markers.

**Available Shapes:**
- `circle` - Perfect circle
- `square` - Square shape
- `triangle` - Triangle pointing up
- `diamond` - Diamond/rhombus shape
- `cross` - X-shaped cross
- `plus` - Plus sign (+)
- `star` - 5-pointed star
- `hexagon` - Regular hexagon
- `pentagon` - Regular pentagon
- `octagon` - Regular octagon

```python
import xatra
from xatra import Icon, ShapeType
from xatra.loaders import gadm

# Create a map
map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# Example 1: Basic geometric shapes
circle_icon = Icon.geometric("circle", color="red", size=24)
map.Point(label="Red Circle", position=[28.6, 77.2], icon=circle_icon)

square_icon = Icon.geometric("square", color="blue", size=24)
map.Point(label="Blue Square", position=[19.0, 73.0], icon=square_icon)

triangle_icon = Icon.geometric("triangle", color="green", size=24)
map.Point(label="Green Triangle", position=[12.3, 76.6], icon=triangle_icon)

# Example 2: Using ShapeType enum
diamond_icon = Icon.geometric(ShapeType.DIAMOND, color="purple", size=24)
map.Point(label="Purple Diamond", position=[10.8, 78.7], icon=diamond_icon)

# Example 3: Shapes with borders
star_icon = Icon.geometric(
    "star", 
    color="yellow", 
    size=28,
    border_color="black", 
    border_width=2
)
map.Point(label="Star with Border", position=[25.0, 121.5], icon=star_icon)

# Example 4: Custom sizes and anchors
large_cross = Icon.geometric(
    "cross",
    color="orange",
    size=32,
    icon_size=(32, 32),
    icon_anchor=(16, 16)
)
map.Point(label="Large Cross", position=[37.9, 23.7], icon=large_cross)

# Available shapes: circle, square, triangle, diamond, cross, plus, star, hexagon, pentagon, octagon

# Example 5: Using single integers (converted to tuples automatically)
city_icon = Icon.geometric("circle", color="navy", icon_size=12, icon_anchor=6)
port_icon = Icon.geometric("square", color="teal", icon_size=12, icon_anchor=6)
map.Point(label="City", position=[39.9, 116.4], icon=city_icon)
map.Point(label="Port", position=[35.0, 136.0], icon=port_icon)

# Export the map
map.show(out_json="tests/geometric_icons.json", out_html="tests/geometric_icons.html")
print("Map with geometric icons exported to geometric_icons.html")
```

#### Complete Icon Example

Here's a comprehensive example showing all icon approaches together:

```python
import xatra
from xatra import Icon, ShapeType
from xatra.loaders import gadm

# Create a map
map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# 1. External URL icons
red_marker = Icon(
    icon_url="https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png",
    shadow_url="https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
    shadow_size=(41, 41)
)
map.Point(label="Custom Red Marker", position=[19.0, 73.0], icon=red_marker)

# 2. Bootstrap Icons (standard icon library)
temple = Icon.bootstrap("bank2", icon_size=26)
map.Point(label="Sacred Temple", position=[13.0, 80.2], icon=temple)

fort = Icon.bootstrap("shield-fill", icon_size=26)
map.Point(label="Fortified Site", position=[23.0, 72.6], icon=fort)

# 3. Leaflet marker built-ins
leaflet_red = Icon.builtin("marker-icon-red.png")
map.Point(label="Leaflet Marker", position=[28.6, 77.2], icon=leaflet_red)

# 4. Geometric shape icons
circle = Icon.geometric("circle", color="blue", size=24)
map.Point(label="Blue Circle", position=[12.3, 76.6], icon=circle)

star = Icon.geometric("star", color="gold", size=28, border_color="black", border_width=2)
map.Point(label="Gold Star", position=[25.0, 121.5], icon=star)

# Using single integers (converted to tuples automatically)
diamond = Icon.geometric("diamond", color="purple", icon_size=20, icon_anchor=10)
map.Point(label="Purple Diamond", position=[10.8, 78.7], icon=diamond)

map.TitleBox("<b>Complete Icon Example</b><br>External URLs, Bootstrap Icons, Leaflet markers, and geometric shapes")

map.show(out_json="tests/complete_icons.json", out_html="tests/complete_icons.html")
print("Complete icon example exported to complete_icons.html")
```

### Point, Path, and River Labels

By default, Points, Paths, and Rivers display their labels in tooltips (on hover). You can optionally display the label directly on the map next to the element using the `show_label` parameter.

For Points, setting `show_label=True` displays the label to the right of the point marker:

```python
import xatra
from xatra.loaders import gadm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# Default: label appears in tooltip on hover
map.Point(label="Mumbai", position=[19.0, 73.0], note="Financial capital of India")

# With show_label: label appears next to the point (note still appears in tooltip)
map.Point(label="Delhi", position=[28.6, 77.2], note="Capital of India", show_label=True)

# Custom hover radius: easier to click on small points
map.Point(label="Small Village", position=[20.5, 75.0], hover_radius=40)

# Custom CSS classes: applied to both marker and label
map.Point(label="Important City", position=[15.5, 78.0], classes="important-point large-marker", show_label=True)

map.show()
```

For Paths, setting `show_label=True` calculates the midpoint along the path (by distance, not by index) and displays the label there. The label is automatically rotated to match the direction of the path at that point:

```python
import xatra
from xatra.loaders import gadm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# Default: label appears in tooltip on hover
map.Path(label="Northern Route", value=[[28,77],[30,90],[35,100]], note="Ancient trade route through northern India")

# With show_label: label appears at the midpoint of the path, rotated to match the path direction (note still appears in tooltip)
map.Path(label="Silk Road", value=[[28,77],[30,90],[40,120]], note="Famous trade route connecting East and West", show_label=True)

# Multiple labels: display label at multiple evenly-spaced positions
map.Path(label="Long Trade Route", value=[[28,77],[30,90],[35,100],[40,120]], show_label=True, n_labels=3)

# Custom hover radius: easier to hover over
map.Path(label="Ancient Trade Route", value=[[25,75],[30,85]], hover_radius=20)

map.show()
```

**Label Rotation and Positioning:** Path labels are intelligently rotated to align with the path direction and offset perpendicular to the path for better readability. The algorithm:
1. Estimates the label length based on the number of characters
2. Finds path points within that distance on either side of each label position
3. Calculates the angle between those points
4. Rotates the label to match, while keeping text readable (never upside down)
5. Translates the label 8px perpendicular to the path to avoid overlapping with the path line

**Multiple Labels:** Use `n_labels` parameter to display multiple copies of the label along the path. Labels are placed at positions `k/(n+1)` where `k = 1, 2, ..., n`. For example:
- `n_labels=1` (default): One label at 1/2 (midpoint)

### Tooltip Notes

The `note` parameter allows you to add additional information that appears in hover tooltips for Flag, River, Path, Point, and Text objects. Notes are displayed in the format "Label — Note" in the tooltip and do not appear in map labels.

```python
import xatra
from xatra.loaders import gadm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# Flag with note
map.Flag(label="Maurya Empire", value=gadm("IND"), note="Largest empire in ancient India")

# River with note
map.River(label="Ganges", value=river_geometry, note="Sacred river of Hinduism")

# Path with note
map.Path(label="Silk Road", value=[[40,74],[35,103]], note="Ancient trade route connecting East and West")

# Point with note
map.Point(label="Delhi", position=[28.6, 77.2], note="Capital of India since 1911")

# Point with note and custom classes
map.Point(label="Mumbai", position=[19.0, 73.0], note="Financial capital of India", classes="important-city")

# Text with note
map.Text(label="Ancient City", position=[28.6139, 77.2090], note="Founded in 736 CE")

# Note appears in tooltip but not in map labels
map.Point(label="Mumbai", position=[19.0, 73.0], note="Financial capital", show_label=True)
# The label "Mumbai" appears on the map, but the note only appears when hovering

map.Text(label="Sacred Site", position=[27.1751, 78.0421], note="Built in 1631-1653", classes="historical-text")
# The text "Sacred Site" appears permanently on the map, but the note only appears when hovering

map.show()
```

**Important:** Notes only appear in hover tooltips and are never displayed as map labels, ensuring clean map appearance while providing additional context on demand.
- `n_labels=2`: Two labels at 1/3 and 2/3
- `n_labels=3`: Three labels at 1/4, 2/4, and 3/4
- `n_labels=5`: Five labels at 1/6, 2/6, 3/6, 4/6, and 5/6

#### River Labels

For Rivers, setting `show_label=True` places the label at an intelligent location on the river. Rivers can be complex MultiLineString geometries with disconnected segments:

```python
import xatra
from xatra.loaders import gadm, naturalearth

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)
map.Flag(label="India", value=gadm("IND"))

# Default: label appears in tooltip on hover
map.River(label="Ganga", value=naturalearth("1159122643"))

# With show_label: label appears on the river, rotated to match the river direction
map.River(label="Yamuna", value=naturalearth("1159122644"), show_label=True)

# Multiple labels: useful for long rivers
map.River(label="Nile", value=naturalearth("1159122999"), show_label=True, n_labels=5)

# Custom hover radius: easier to hover over thin rivers
map.River(label="Tributary", value=naturalearth("1159122650"), hover_radius=25)

map.show()
```

**River Label Algorithm:** For rivers with complex geometries (including MultiLineString with potentially disconnected segments):
1. Samples points from all river segments (up to 200 points for performance)
2. Finds the two most distant points along the river geometry (the pair that maximizes river distance)
3. For each label position `k/(n+1)`, interpolates along the line between these distant points
4. Finds the nearest point on the actual river geometry to each interpolated position
5. Places labels at these nearest points on the river
6. Calculates rotation angle based on the line between the two distant points
7. Translates each label 12px perpendicular to the river for better visibility

This approach ensures labels are distributed along the actual river course rather than just across a bounding box, providing much better geographic distribution for long, complex rivers.

**Multiple Labels:** The `n_labels` parameter distributes labels evenly along the river's longest axis:
- `n_labels=1` (default): One label at the midpoint along the river's longest dimension
- `n_labels=3`: Three labels distributed along the river's extent
- `n_labels=5`: Five labels for very long rivers like the Nile or Ganges, spread from end to end

**Styling Labels:**

You can style Point, Path, and River labels using CSS. Labels have the classes `point-label`, `path-label`, and `river-label` respectively, in addition to the `text-label` class.

**Customizing Offset Distance:** 

Labels use nested divs to separate rotation (calculated automatically) from translation (customizable via CSS):
- **Outer div**: Applies rotation based on path/river direction (set via inline style, not customizable)
- **Inner div**: Applies perpendicular offset using `transform: translateY()` (fully customizable via CSS)

This structure allows you to customize the offset distance without affecting the rotation. Default offsets are:
- **Points**: 10px to the right
- **Paths**: 8px perpendicular
- **Rivers**: 16px perpendicular

To customize the offset, simply override the `transform` property on the label class:

```python
map.CSS("""
.point-label {
  font-size: 14px;
  font-weight: bold;
  color: #cc0000;
  background: rgba(255,255,255,0.8);
  padding: 2px 6px;
  border-radius: 3px;
}

.path-label {
  font-size: 16px;
  color: #0066cc;
  font-style: italic;
  background: rgba(255,255,255,0.9);
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid #0066cc;
  /* Customize offset distance (default is -8px) */
  transform: translateY(-12px);
}

.river-label {
  font-size: 15px;
  color: #0099cc;
  font-weight: bold;
  background: rgba(255,255,255,0.85);
  padding: 3px 7px;
  border-radius: 3px;
  border: 1px solid #0099cc;
  /* Customize offset distance (default is -16px) */
  transform: translateY(-20px);
}

/* You can also use custom classes */
.trade-route .path-label {
  color: #ff9900;
  border-color: #ff9900;
}

.sacred-river .river-label {
  color: #ff6600;
  border-color: #ff6600;
  /* Increase offset for this specific river */
  transform: translateY(-24px);
}
""")

map.Path(label="Trade Route", value=[[28,77],[30,90],[40,120]], show_label=True, classes="trade-route")
map.River(label="Ganga", value=naturalearth("1159122643"), show_label=True, classes="sacred-river")
```

**Label Rotation:**

Point and Text labels support rotation via the `rotation` parameter or CSS:

```python
# Rotate labels via API
map.Point("City", [28.6, 77.2], show_label=True, rotation=45)
map.Text("Label", [28.6, 77.2], rotation=30)

# Or via CSS (rotation and positioning are separated)
map.CSS("""
.rotated-label .point-label {
  transform: translateX(10px) rotate(15deg) !important;
}
""")
```

### Multi-Layer Tooltips

Xatra features an intelligent **multi-layer tooltip system** that shows information for **all overlapping elements** at the cursor position, not just the topmost element.

When you hover over any location on the map, the tooltip displays information from all elements under the cursor:
- **Flags** (countries/kingdoms)
- **Admin regions** (states, districts, tehsils)
- **DataFrames** (data values)
- **Rivers** and **Paths**
- **Points** (cities, landmarks)

The tooltips are displayed in a clean, organized format with each element type clearly labeled.

**Example:**
```python
import xatra
from xatra.loaders import gadm

map = xatra.Map()
map.BaseOption("Esri.WorldTopoMap", default=True)

# These elements overlap - hovering over Tamil Nadu will show all tooltips
map.Flag(label="India", value=gadm("IND"), note="Republic of India")
map.Flag(label="Tamil Nadu", value=gadm("IND.31"), note="State in southern India")
map.Admin(gadm="IND.31", level=2)  # Districts

# Point in the overlapping area - hovering near Chennai shows Flag + Admin + Point tooltips
map.Point(label="Chennai", position=[13.0827, 80.2707])

map.show()
```

When you hover over Chennai, you'll see tooltips for:
- The "India" flag
- The "Tamil Nadu" flag
- The admin region (Chennai district)
- The "Chennai" point marker

#### Customizing Hover Detection Radius

For **Point**, **River**, and **Path** elements, you can customize the hover detection radius using the `hover_radius` parameter (in pixels). This controls how close your cursor needs to be to trigger the tooltip.

```python
# Rivers with larger hover radius for easier selection
map.River(label="Ganga", value=naturalearth("1159122643"), hover_radius=20)

# Paths with custom hover radius
map.Path(label="Silk Road", value=[[35, 75], [40, 80]], hover_radius=15)

# Points with larger hover radius for easier clicking
map.Point(label="Delhi", position=[28.6, 77.2], hover_radius=30)
```

**Default hover radii:**
- **Rivers**: 10 pixels
- **Paths**: 10 pixels  
- **Points**: 20 pixels

**Note:** The hover radius is specified in screen pixels and automatically scales with map zoom level. A larger hover radius makes elements easier to hover over, especially useful for thin rivers or small points.

#### Customizing Multi-Tooltip Styling

The multi-layer tooltip appearance can be customized using CSS:

```python
map.CSS("""
/* Customize the multi-tooltip container */
#multi-tooltip {
  background: rgba(255, 255, 255, 0.98);
  border: 3px solid #0066cc;
  border-radius: 8px;
  font-family: 'Georgia', serif;
  max-width: 500px;
}

/* Style the element type labels */
#multi-tooltip .tooltip-type {
  color: #cc6600;
  font-weight: bold;
  font-size: 13px;
  text-transform: uppercase;
}

/* Style the tooltip content */
#multi-tooltip .tooltip-content {
  color: #333;
  font-size: 14px;
  line-height: 1.6;
}

/* Style individual tooltip items */
#multi-tooltip .tooltip-item {
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 2px solid #ddd;
}
""")
```

## Performance

Xatra is optimized for large, complex maps with many elements:

- **Geometry Registry (De-duplication)**: Map snapshots and elements reference a central geometry library. Identical coordinates are stored only once in the JSON/HTML, resulting in up to **95% reduction in redundancy** for complex dynamic maps.
- **Geometry Caching**: Territory geometries are cached globally by string representation, with both in-memory and on-disk persistence. Identical territory expressions (e.g., `gadm("IND") | gadm("PAK")`) are computed only once and reused across all instances.
- **Binary Data Loading**: GADM and Natural Earth files use a persistent binary cache (`.pickle`). After the first run, data loading is up to **70% faster** than parsing raw JSON.
- **Serialization Reuse**: The export process serializes the map payload once and reuses it for both JSON and HTML outputs, bypassing expensive redundant processing.
- **Centroid Pre-computation**: Centroids are calculated once during export and stored in the registry, not on every render.
- **Layer Visibility Toggling**: Dynamic maps use efficient visibility toggling instead of recreating layers.
- **High-Speed JSON**: Supports `orjson` for lightning-fast coordinate array serialization and parsing.
- **Memory Management**: Use `clear_file_cache()` to free memory when working with very large datasets.

For faster geometry operations, you can precompute topology-safe simplified GADM copies:

```bash
xatra-simplify-data
```

Then select simplification per map:

```python
import xatra
map = xatra.Map()
map.simplify(0.025)
# or pyplot-style
xatra.simplify(0.025)
```

### Geometry Cache Management

The global geometry cache can be managed programmatically:

```python
import xatra

# Clear cache
xatra.clear_cache()                    # Clear both memory and disk
xatra.clear_cache(memory_only=True)    # Clear only memory
xatra.clear_cache(disk_only=True)      # Clear only disk

# Get cache statistics
stats = xatra.cache_stats()
print(f"Hit rate: {stats['hit_rate']:.1%}")
print(f"Memory cache: {stats['memory_cache_size']} items")
print(f"Disk cache: {stats['disk_cache_size']} files")
```

Cache files are stored in `~/.xatra/cache/` and persist across program runs for maximum performance.

To disable cache usage globally for a run, set:

```bash
CACHING=False python your_script.py
```

Accepted false values are `0`, `false`, `no`, `off` (case-insensitive). When disabled, Xatra bypasses on-disk geometry cache reads/writes while keeping in-process memory reuse, and does not clear existing cache contents.

### Time Debugging

Xatra includes a comprehensive time debugging feature that helps you understand where time is being spent when creating maps. When enabled, it prints detailed timing information for every major operation with HH:MM:SS timestamps and tracks **exclusive time** (time spent in each function excluding time spent in other tracked functions called from it).

#### Enabling Time Debugging

**Method 1: Environment Variable (Recommended)**
```bash
DEBUG_TIME=1 python your_script.py
```

**Method 2: Programmatically**
```python
import xatra

xatra.set_debug_time(True)

# Now create your map - all operations will be timed
map = xatra.Map()
map.Flag("India", xatra.gadm("IND"))
map.show()
```

**Environment Variable Values:**
- `DEBUG_TIME=1`, `true`, `yes`, `on` → Enable debugging
- `DEBUG_TIME=0`, `false`, `no`, `off` → Disable debugging (default)
- Unset or empty → Disable debugging (default)

When time debugging is enabled, you'll see timing information for:

**Data Loading Operations:**
- Loading GADM data files
- Loading Natural Earth features
- Loading Overpass data (local cache lookup + optional on-demand API fetch)
- Reading JSON files from disk (with cache hits/misses)
- Converting GeoJSON to Shapely geometries

**Map Element Operations:**
- Adding Flags, Rivers, Paths, Points, Text, etc.
- Adding Admin regions
- Adding DataFrames

**Processing Operations:**
- Territory geometry conversions
- Pax-max aggregation for dynamic maps
- Centroid calculations
- Period filtering

**Export Operations:**
- Exporting to JSON format
- Exporting to HTML format

#### Exclusive Time Tracking

The time debugging system tracks two types of time for each function:

- **Total Time**: The complete time spent in the function, including time spent in other tracked functions called from it
- **Exclusive Time**: The time spent in the function itself, excluding time spent in other tracked functions called from it

This helps identify which functions are doing the most actual work versus just calling other functions.

```
[14:23:45] → START: Add Flag
[14:23:45]   args: 'India', <Territory object at 0x...>
[14:23:45]   → START: Load GADM data
[14:23:45]     → START: Load GADM-like data
[14:23:45]       → START: Read JSON file
[14:23:45]       ✓ FINISH: Read JSON file
[14:23:45]     ✓ FINISH: Load GADM-like data
[14:23:45]   ✓ FINISH: Load GADM data
[14:23:45] ✓ FINISH: Add Flag
[14:23:46] → START: Show (export map)
[14:23:46]   → START: Export to JSON
[14:23:46]     → START: Paxmax aggregation
[14:23:46]     ✓ FINISH: Paxmax aggregation
[14:23:47]   ✓ FINISH: Export to JSON
[14:23:47]   → START: Export to HTML
[14:23:47]   ✓ FINISH: Export to HTML
[14:23:47] ✓ FINISH: Show (export map)
```

#### Disabling Time Debugging

**Method 1: Environment Variable**
```bash
unset DEBUG_TIME
# or
export DEBUG_TIME=0
```

**Method 2: Programmatically**
```python
# Disable time debugging
xatra.set_debug_time(False)
```

**Note:** The programmatic methods override the environment variable setting.

#### Automatic Timing Display

When time debugging is enabled, timing statistics and charts are **automatically displayed** when your program exits. This means you don't need to manually call any functions - the performance analysis appears automatically!

#### Manual Timing Analysis

You can also manually analyze the timing data if needed:

**Print Timing Summary:**
```python
import xatra

# Print a formatted table of timing statistics
xatra.print_timing_summary()
```

**Get Raw Timing Data:**
```python
# Get timing statistics as a dictionary
stats = xatra.get_timing_stats()
print(f"Functions tracked: {len(stats['exclusive_times'])}")
print(f"Total function calls: {sum(stats['call_counts'].values())}")
```

**Create Timing Charts:**
```python
# Display an interactive timing chart (requires matplotlib)
xatra.show_timing_chart()

# Save timing chart to file
xatra.plot_timing_chart(save_path="timing_analysis.png")

# Get chart without displaying it
fig = xatra.plot_timing_chart(show_chart=False)
```

**Reset Timing Statistics:**
```python
# Clear all timing data
xatra.reset_timing_stats()
```

The timing chart shows:
- **Top chart**: Horizontal bar chart of exclusive times for the top 15 functions
- **Bottom chart**: Side-by-side comparison of exclusive vs total times
- **Summary statistics**: Total times, function counts, and call counts

**Automatic Display Features:**
- Timing statistics are automatically printed when the program exits
- Charts are automatically displayed (if matplotlib is available)
- No manual intervention required - just enable debug time and run your code
- Works with both environment variable (`DEBUG_TIME=1`) and programmatic (`xatra.set_debug_time(True)`) methods

## Data Sources

### GADM (Global Administrative Areas)
- Country codes: "IND", "PAK", "CHN", etc.
- Subdivisions: "IND.31", "IND.31.1", etc.
- Files: `data/gadm/gadm41_*.json`

### Natural Earth
- Rivers: `data/ne_10m_rivers.geojson`
- Features identified by `ne_id` property

### Overpass API
- Rivers: `data/rivers_overpass_india/`
- Features identified by OSM ID in filename
- Auto-cached files are named like `overpass_relation_<id>.json` or `overpass_way_<id>.json`
- `overpass(osm_id, osm_type="relation"|"way")` restricts both local filename matching and API lookup

## TODO

## Usage/examples
- [x] copy maps from old xatra (colonies and hsr remaining)
- [x] redirect old map links
- [ ] maps of north-west: Panini, Puranas and Greek
- [ ] full history timeline
- [x] GDP per capita map
- [x] admin map

## Features
- [x] adding music to maps
    ```
    Understand how this project works from the README, and add the following feature to xatra: a map.Music("blabla.mp3") (and corresponding xatra.Music()) layer. It should take a path to an audio file and optionally parameters timestamps=(...,...), period=(...,...) which determine what portion of the audio file is played between what years on the slider.

    If the map is static (i.e a slider never appears, because none of the elements have periods), there should simply be play and pause controls for the music, and it should continuously play on repeat. If the map is dynamic, the music will instead play in alignment with the slider based on the following logic:

    If period is None (the default), it is taken to be the whole period of the slider (make sure you find out where this is calculated and reuse existing functions/code).
    If timestamps is None (the default), it is taken to be the whole length of the audio.
    The segment of the music between timestamps[0] and timestamps[1] should start playing from period[0] till period[1] at its own pace, and if the music ends (i.e. timestamps[1] is reached) before reaching period[1] it should simply repeat. 

    Note that the slider years and speed have defaults/are computed even when there no explicit .slider() method is called in constructing the map---when .slider() is called, it overrides these computed defaults.
    ```
- [x] Vassals via slash-separated labels (for example `India/Karnataka`), with light HSLA overlay colors and preserved parent coverage.
- [x] Actually I don't like this implementation of vassals. Better to have vassals with slash-separated names, and their colors should always be something like hsla(randomly-generated hue, 100% saturation, 90% luminosity, 60% alpha). This makes the vassals just a lightening filter over their parent's territories. Implement it and write a simple tests/example_vassals.py file (no actual pytest tests, just an example map like in tests/example.py etc.) to demonstrate that it works. I do _not_ want to cut the children out of their parents' terrritories etc. as that is important for other things like border highlighting and label placement.
- [x] Introduce a parameter to Flag called `display_label: Optional[str]`. By default, the label displayed for a Flag is its `label` (which also controls the paxmax aggregation of different Flag elements and over time, assigning colors etc.)---we should be able to override just the label displayed with `display_label` (while `label` still controls all the internals). The idea behind this is to: (1) allow more complex HTML as the display label rather than plain text and (2) to allow different display labels for the same flag at different points in time. Now obviously for any particular point in time, Flags with the same label must have the same displayed label too (since they are literally merged to display a single displayed label for both)---which should be the last-declared one---but this will allow the map's displayed label to change with time, e.g. to show the succession of different kings over generations. You may need to understand the paxmax aggregation carefully to implement this properly---then run `example_display_label.py` so that I can check your work has the intented behaviour.
- [x] Custom polygon territory --- just like how we can add paths, but these can be treated as actual shapes that can be treated as territories (i.e. we can take unions, subtractions and intersections of them with any other territory)
- [x] search feature: search elements (feature search + geocoder in TitleBox)
- [x] Orient flag labels in direction of flag
- [x] option for different point markers besides pin
- [x] option for point labels, path, river labels
- [x] option to create multiple path, river labels.
- [x] And maybe calculate river label path using bounding box.
- [x] ideally make it so hovering hovers on *all* flags/elements at that point
- [x] "get current map" similar to matplotlib, to make maps more modular
- [x] Hover over color bar
- [x] periods for things other than flags
- [x] xatra.Admin
- [x] xatra.Flag color groups
- [x] xatra.Dataframe with notes and DataColormap
- [x] do we need to redraw everything each frame?
- [x] Geometry Registry: de-duplicate geometries across snapshots
- [ ] MAYBE: class-based show labels, etc. Not that important. Main thing you'd use it for is hiding labels and CSS is enough for that.
- [ ] MAYBE: grouping of map elements and layer selection. PROBLEM: this is hard.
- [ ] MAYBE: calculate and keep simplified geometries (check what the main source of slowness is) PROBLEM: boundaries between different geometries no longer fit perfectly -- use mapshaper
- [x] loading geojson from library/registry instead of repeating it in html; i.e. with hydration

## Dev
- [x] Get it in a publishable state
- [x] why not just upload my cache. Need to change the HF repository to have a data and a cache folder
- [x] Publish it
- [x] time debugging

## Stylistic changes
- [x] The slider seems to be hidden under the map: I can move it around by clicking where it should be, but can only actually see it visually when I'm zooming out or am fully zoomed out (because that's when the blank space behind the map appears before the tiles load to fill it up). The slider should be a fixed element on the screen *over* the map that stays at the exact same position regardless of where I pan or zoom to.
- [x] the TitleBox only appears when fully zoomed out. It too should appear as a fixed element on the screen *over* the map that stays at the exact same position regardless of where I pan or zoom to.
- [x] custom IDs and classes for styling
- [x] The tooltip that appears upon hovering over a flag should appear at the point of my cursor, and move with my cursor. I thought this is how default Leaflet tooltips appear? Why does it appear at a fixed point in our implementation?
- [x] also need flag names to appear at centroid
- [x] map.Text labels should by default just be plain text, without the border box and all that. Its default style could be different maybe "font-size: 16px; font-weight: bold; color: #666666"
- [x] color assignment
- [x] t logachoice of BaseMaps
- [x] slider shouldn't appear for static maps
- [x] map.slider()
- [x] slider play button plus positions of years, start and end year
- [x] watermark

## Libraries
- [x] copy matchers from old xatra

## Bugfixes
- [x] Better documentation for icons
- [x] classes for Points that affect their labels
- [x] center map at center; and allow setting zoom
- [x] notes for Points
- [x] tooltips and notes for Texts
- [x] Efficiency: cache territory geometries by string rep rather than per-object
- [x] optimize paxmax aggregation
- [x] AdminRivers don't work again.
- [x] ~~show bounding boxes of flag paxmaxes when selected~~ instead just show outline on hover
- [x] AdminRivers doesn't work.
- [x] River name rendering position is weird
- [x] River names don't render for overpass
- [x] color bar hover on correct position
- [x] Dataframe should not work the stupid way it does. It should be a simple chloropleth, not creating new geometries for each year.
- [x] Static Dataframe maps don't work
- [x] logarithmic/normalized color bars with matplotlib Normalize support
- [x] Make sure color bars are shown correctly regardless of how it is
- [x] Support a "note" column for Dataframe.
- [x] Don't assume missing values
- [x] classes attribute not being passed on
- [x] disputed areas -- should show up for Admin, and more importantly should be able to specify source file
- [x] disputed areas admin map
- [x] fix issue with disputed areas
- [x] top-level disputed areas
- [x] debug slowness
- [x] xatra.Data issue with colors of data in dynamic maps and exact range
- [x] sub-regions -- use boundary-aware starts matching, same as elsewhere
- [x] Mark map as dynamic if any element as period
- [x] why country name appears on Data tooltip?
- [x] default color scheme
- [x] add color map in html
- [x] rename Colormap to Colormap
- [x] efficiency and documentation
- [x] [WONTFIX] make outline of rivers show on hover too -- need rivers to be grouped for that
- [x] [WONTFIX] Point labels should align to exact position. It's OK, just set margins
