The matplotlib of maps - create beautiful historical, administrative, and data maps
Project description
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.
Installation
pip install xatra
xatra-install-data
Or if installing to a local virtual environment:
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 to ~/.xatra/data/.
Old alpha version of xatra. Always make sure you're using version 2.11 or later.
Example: Historical map
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:
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
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:
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:
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:
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 -- 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:
#!/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 dogadm("Z01.14"),gadm("Z01.14.1")etc. There are also custom territoriesZ01,Z02etc. that you can import fromxatra.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 regionsmap.Flag()- When usinggadm()loadermap.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.
#!/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 uselib = xatrahub("...")thenlib.TERRITORY_NAME.kind="map"orkind="css": applies imported code and returnsNone.filter_only/filter_not(lists of method names like["Flag", "River"]) filter importedxatra.<Method>(...)calls for map/css imports.
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):
- 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.
- 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)beforemap.show()— e.g.map.Geocoder("mapbox", api_key="pk.xxx")ormap.Geocoder("google", api_key="..."). Supported providers:nominatim,mapbox,google,photon.
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.
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). Usetype="vassal"ortype="province"for slash-separated hierarchical labels likeIndia/Karnataka. Useinherit="OtherFlag"to force color inheritance from an earlier-defined flag.Dataframe(dataframe, data_column=None, year_columns=None, classes=None): Add DataFrame-based choropleth dataAdmin(gadm, level, period=None, classes=None, color_by_level=1): Add administrative regions from GADM dataAdminRivers(period=None, classes=None, sources=None): Add rivers from specified data sourcesRiver(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 radiusPath(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 radiusPoint(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 classesText(label, position, note=None, classes=None, period=None): Add a text label with optional tooltip noteTitleBox(html, period=None): Add a title box with HTML contentGeocoder(provider="nominatim", api_key=None): Set the place-search (geocoder) provider. Usenominatim(default),mapbox,google, orphoton. 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 stylesBaseOption(url_or_provider, name=None, default=False): Add base map layer.url_or_providermay 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, orStadia.StamenTerrainBackground. For Stadia providers and rawstadiamaps.comtile URLs, setSTADIA_API_KEYin.env.FlagColorSequence(color_sequence, class_name=None): Set the color sequence for flagsAdminColorSequence(color_sequence): Set the color sequence for admin regionsDataColormap(colormap, vmin=None, vmax=None, norm=None): Set the color map for data elementszoom(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 renderedFlag/Admin/Data/Dataframegeometries.River/Path/Point/Textare 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 (Nonedisables) [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 existsset_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 mapxatra.River(...): Add a river to the current mapxatra.Path(...): Add a path to the current mapxatra.Point(...): Add a point to the current mapxatra.Text(...): Add a text label to the current mapxatra.TitleBox(...): Add a title box to the current mapxatra.Geocoder(...): Set geocoder provider for the current mapxatra.Admin(...): Add administrative regions to the current mapxatra.AdminRivers(...): Add rivers to the current mapxatra.Dataframe(...): Add DataFrame data to the current map
Styling and Configuration
xatra.CSS(...): Add CSS styles to the current mapxatra.BaseOption(...): Add a base map layer to the current mapxatra.FlagColorSequence(...): Set flag color sequence for the current mapxatra.AdminColorSequence(...): Set admin color sequence for the current mapxatra.DataColormap(...): Set data colormap for the current mapxatra.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:
# 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:
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:
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 objectfrom_gadm(key): Create from GADM administrative boundaryfrom_naturalearth(ne_id): Create from Natural Earth datasetfrom_polygon(coords, holes=None): Create from custom polygon coordinates
Operations
territory1 | territory2: Union of two territoriesterritory1 - territory2: Difference of two territoriesterritory1 & 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 IDoverpass(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_typecan 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.
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 asPath()andPoint()) - 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 colorsRandomColorSequence(): Generates random colors
Examples
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:
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
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 useshsla(random_hue, 100%, 90%, 0.6)type="province": fill is transparent, border is thin withopacity: 0.8in root parent color- Children are classed as
.vassalor.provincefor custom CSS - Child labels use leaf names (for example
VidisaforMaurya/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:
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:
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
notecolumn 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)_notecolumns for year-specific tooltip information - Auto-detected when DataFrame has multiple numeric columns
Note Columns:
- Static maps: Use a column named
notefor 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 columnsdata_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 stylingfind_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:
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:
# 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] listclasses: 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:
# 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] listclasses: Optional CSS classes for stylingsources: 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:
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 set from jsDelivr:
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=24icon_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 markermarker-icon-red.png- Red markermarker-icon-green.png- Green marker
# 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 circlesquare- Square shapetriangle- Triangle pointing updiamond- Diamond/rhombus shapecross- X-shaped crossplus- Plus sign (+)star- 5-pointed starhexagon- Regular hexagonpentagon- Regular pentagonoctagon- Regular octagon
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:
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:
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:
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:
- Estimates the label length based on the number of characters
- Finds path points within that distance on either side of each label position
- Calculates the angle between those points
- Rotates the label to match, while keeping text readable (never upside down)
- 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.
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/3n_labels=3: Three labels at 1/4, 2/4, and 3/4n_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:
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):
- Samples points from all river segments (up to 200 points for performance)
- Finds the two most distant points along the river geometry (the pair that maximizes river distance)
- For each label position
k/(n+1), interpolates along the line between these distant points - Finds the nearest point on the actual river geometry to each interpolated position
- Places labels at these nearest points on the river
- Calculates rotation angle based on the line between the two distant points
- 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 dimensionn_labels=3: Three labels distributed along the river's extentn_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:
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:
# 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:
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.
# 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:
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
orjsonfor 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:
xatra-simplify-data
Then select simplification per map:
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:
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:
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)
DEBUG_TIME=1 python your_script.py
Method 2: Programmatically
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 debuggingDEBUG_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
unset DEBUG_TIME
# or
export DEBUG_TIME=0
Method 2: Programmatically
# 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:
import xatra
# Print a formatted table of timing statistics
xatra.print_timing_summary()
Get Raw Timing Data:
# 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:
# 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:
# 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_idproperty
Overpass API
- Rivers:
data/rivers_overpass_india/ - Features identified by OSM ID in filename
- Auto-cached files are named like
overpass_relation_<id>.jsonoroverpass_way_<id>.json overpass(osm_id, osm_type="relation"|"way")restricts both local filename matching and API lookup
TODO
Usage/examples
- copy maps from old xatra (colonies and hsr remaining)
- redirect old map links
- maps of north-west: Panini, Puranas and Greek
- full history timeline
- GDP per capita map
- admin map
Features
- 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. - Vassals via slash-separated labels (for example
India/Karnataka), with light HSLA overlay colors and preserved parent coverage. - 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.
- Introduce a parameter to Flag called
display_label: Optional[str]. By default, the label displayed for a Flag is itslabel(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 withdisplay_label(whilelabelstill 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 runexample_display_label.pyso that I can check your work has the intented behaviour. - 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)
- search feature: search elements (feature search + geocoder in TitleBox)
- Orient flag labels in direction of flag
- option for different point markers besides pin
- option for point labels, path, river labels
- option to create multiple path, river labels.
- And maybe calculate river label path using bounding box.
- ideally make it so hovering hovers on all flags/elements at that point
- "get current map" similar to matplotlib, to make maps more modular
- Hover over color bar
- periods for things other than flags
- xatra.Admin
- xatra.Flag color groups
- xatra.Dataframe with notes and DataColormap
- do we need to redraw everything each frame?
- 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
- loading geojson from library/registry instead of repeating it in html; i.e. with hydration
Dev
- Get it in a publishable state
- why not just upload my cache. Need to change the HF repository to have a data and a cache folder
- Publish it
- time debugging
Stylistic changes
- 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.
- 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.
- custom IDs and classes for styling
- 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?
- also need flag names to appear at centroid
- 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"
- color assignment
- t logachoice of BaseMaps
- slider shouldn't appear for static maps
- map.slider()
- slider play button plus positions of years, start and end year
- watermark
Libraries
- copy matchers from old xatra
Bugfixes
- Better documentation for icons
- classes for Points that affect their labels
- center map at center; and allow setting zoom
- notes for Points
- tooltips and notes for Texts
- Efficiency: cache territory geometries by string rep rather than per-object
- optimize paxmax aggregation
- AdminRivers don't work again.
-
show bounding boxes of flag paxmaxes when selectedinstead just show outline on hover - AdminRivers doesn't work.
- River name rendering position is weird
- River names don't render for overpass
- color bar hover on correct position
- Dataframe should not work the stupid way it does. It should be a simple chloropleth, not creating new geometries for each year.
- Static Dataframe maps don't work
- logarithmic/normalized color bars with matplotlib Normalize support
- Make sure color bars are shown correctly regardless of how it is
- Support a "note" column for Dataframe.
- Don't assume missing values
- classes attribute not being passed on
- disputed areas -- should show up for Admin, and more importantly should be able to specify source file
- disputed areas admin map
- fix issue with disputed areas
- top-level disputed areas
- debug slowness
- xatra.Data issue with colors of data in dynamic maps and exact range
- sub-regions -- use boundary-aware starts matching, same as elsewhere
- Mark map as dynamic if any element as period
- why country name appears on Data tooltip?
- default color scheme
- add color map in html
- rename Colormap to Colormap
- efficiency and documentation
- [WONTFIX] make outline of rivers show on hover too -- need rivers to be grouped for that
- [WONTFIX] Point labels should align to exact position. It's OK, just set margins
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xatra-2.15.0.tar.gz.
File metadata
- Download URL: xatra-2.15.0.tar.gz
- Upload date:
- Size: 184.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81414a678595dea5d60b8546675b60b74fef8016d41541bafaa66ddd889660ff
|
|
| MD5 |
32556e8d42e4cbc1c210f4425f85edd9
|
|
| BLAKE2b-256 |
c54b3e011ccda60f7a626977af0112fbd63a634962ae8a6678413d6719ddf59b
|
File details
Details for the file xatra-2.15.0-py3-none-any.whl.
File metadata
- Download URL: xatra-2.15.0-py3-none-any.whl
- Upload date:
- Size: 136.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7118134d2c8a1b489c0f9af04f594e7bc4ccd7318065e3d48c40c91c3435d05
|
|
| MD5 |
50da5e5908a45cb54753a12a5c6ba00b
|
|
| BLAKE2b-256 |
af97627baaba4abed4dc9270383504aac9f2f83b203c80ce957581f89f4c3399
|