Skip to content

Maps ​

A map in Holochart is a geo subplot: layout.geo holds a map projection, a base map of land, coastlines and borders, and the view. The traces scattergeo and choropleth draw on it. The attributes are Plotly's layout.geo, so a Plotly figure with a projected map renders as it is.

This page covers the subplot, flat and as a 3D globe. One kind of map is not here because it does not exist yet: tile maps (scattermap, choroplethmap, densitymap on streets or imagery). They are planned.

Adding the package ​

Maps live in @mk7s/holochart-traces-geo. It is the one trace package the full bundle does not register, so that apps without a map do not download map code.

With the full bundle, add one import after it. It registers the package and re-exports it:

ts
import { createChart } from '@mk7s/holochart';
import '@mk7s/holochart/geo';

createChart(document.getElementById('chart')!, {
  data: [{ type: 'scattergeo', lon: [-0.13, 2.35, 13.4], lat: [51.51, 48.86, 52.52] }],
  layout: { geo: { projection: { type: 'natural earth' }, fitbounds: false } },
});

Nothing else is installed: @mk7s/holochart depends on the geo package.

With a partial bundle, install the package next to the runtime and register tracesGeo, which holds both traces and the component that draws the subplot:

sh
pnpm add @mk7s/holochart-runtime@alpha @mk7s/holochart-traces-geo@alpha three
ts
import { createChart, register } from '@mk7s/holochart-runtime';
import { tracesGeo } from '@mk7s/holochart-traces-geo';

register(...tracesGeo);

createChart(document.getElementById('chart')!, {
  data: [{ type: 'choropleth', locations: ['FRA', 'DEU'], z: [68, 84] }],
});

A figure with a scattergeo or choropleth trace, or a layout.geo, on a chart without the package is drawn without them, and the console warning names the import that is missing.

The script-tag build has no maps yet.

The geo subplot ​

A geo trace goes on layout.geo unless it names another subplot (geo: 'geo2'). The container is created when a trace needs it, so a figure can leave it out. Without any attribute it is a world map in the equirectangular projection with coastlines and a frame, fitted to the data.

ts
import { createChart } from '@mk7s/holochart';
import '@mk7s/holochart/geo';

createChart(document.getElementById('chart')!, {
  data: [{ type: 'scattergeo', lon: [-74, 2.35, 139.7], lat: [40.7, 48.86, 35.7] }],
  layout: {
    geo: {
      projection: { type: 'orthographic', rotation: { lon: -30, lat: 30 } },
      fitbounds: false,
      showocean: true,
      showcountries: true,
      lonaxis: { showgrid: true },
      lataxis: { showgrid: true },
    },
  },
});
Loading example…
scattergeo/great-circles

What the container holds:

AttributesWhat they set
projection.typeThe projection: see the table below
projection.rotation.lon, .lat, .rollHow the globe is turned before it is projected
projection.scale, center.lon, center.latThe zoom factor (1 fits the ranges into the subplot) and the point in the middle
projection.minscale, projection.maxscaleLimits of zooming by hand
projection.parallelsThe two standard parallels of a conic projection
projection.tilt, projection.distanceThe viewpoint of the satellite projection
lonaxis.range, lataxis.rangeThe longitudes and latitudes in view: the map is fitted to this box and clipped to it
scopeThe part of the world the base map covers: see Scopes
fitboundsFit the view to the data: see Fitting the view
resolutionDetail of the base map, 110 or 50: see What the data costs
showland, showocean, showlakes, showriversFilled land, ocean and lakes, and rivers; each with its …color (rivers: …width)
showcoastlines, showcountries, showsubunits, showframeLines: coasts, country borders, state borders, the outline of the globe; each with its …color and …width
lonaxis / lataxis: showgrid, dtick, tick0, gridcolor, gridwidth, griddashThe graticule: meridians every 30° and parallels every 10° by default
bgcolorThe background inside the map's frame
visiblefalse hides every base layer and the graticule, unless the figure shows one itself
domainWhere the subplot is in the figure: see Several maps

The layout reference has every attribute with its default.

Base layers. Which layers show by default depends on the map: coastlines and the frame on world maps, country borders on scoped maps other than usa, state borders on usa (and on north america at resolution: 50, which also has Canada's provinces). Land, ocean, lakes and rivers are off in the schema, as in Plotly; the default holochart template turns the land and the lakes on and colors them for its dark page. Albers USA draws neither coastlines nor an ocean.

Rotation. projection.rotation turns the globe before it is projected: lon puts another meridian in the middle, lat tips the globe towards a pole, roll turns it about the line of sight. Every rotation moves the place where a flat map is cut:

Loading example…
geo/rotations

Draw order, bottom to top: background, ocean, land, lakes, state borders, country borders, coastlines, rivers, graticule, frame, choropleth regions, scatter traces. On a subplot with a choropleth, rivers and lakes move above the regions.

Scopes ​

scope picks the part of the world the base map covers, and with it a default projection, default ranges and how the map is moved:

scopeDefault projectionShows
'world'equirectangularEverything
'usa'albers usaThe United States, with Alaska and Hawaii as insets, and their states
'europe'conic conformalThe countries of Europe
'asia'mercatorThe countries of Asia
'africa'mercatorThe countries of Africa
'north america'conic conformalThe countries of North America
'south america'mercatorThe countries of South America
'antarctica'equirectangularAntarctica
'oceania'equirectangularThe countries of Oceania

A scoped map shows only the countries of its continent, as Natural Earth assigns them, with the land, lakes and rivers that belong to them: the rest of the world is not drawn, and locations outside the scope match nothing. A country polygon that reaches into the scope's box is kept whole (Russia in europe), and lonaxis.range / lataxis.range frame the map. Overseas parts outside the box are left out (France in europe is without French Guiana).

Loading example…
geo/scope-europe

scope: 'usa' is three projections in one, each clipped to a frame of its own: the contiguous states, Alaska and Hawaii. Nothing is drawn between the frames, lines included:

Loading example…
geo/usa-insets

A scoped map pans and zooms and never rotates. To show a continent on a map that turns, or with countries of the neighbouring continent, use scope: 'world' and set the ranges yourself:

Loading example…
choropleth/country-names

An albers usa map is always the usa scope.

Projections ​

projection.type takes Plotly's names, and one Holochart extra. Plotly's are all drawn by d3-geo and d3-geo-projection, on the CPU, with d3's clipping at the edge of the projection, cutting at the antimeridian and resampling of long edges into curves.

Loading example…
geo/projections

projection.type takes Plotly's 84 names (82 projections: 2 are second names of another). 16 are in the package's own code and 68 in its lazy chunk. It also takes globe3d, a Holochart extra that is not a Plotly projection.

A figure that names a projection of the lazy chunk loads it once, with a dynamic import(); the map is drawn when the chunk has arrived, and chart.ready waits for it.

The last two columns say what a drag does on a world map of that type (a scoped map always pans, see Interactions) and whether fitbounds can fit it.

In the package's own code ​

The projections d3-geo has. They cost nothing beyond the package itself.

projection.typed3 projectionA drag on a world mapfitbounds
albersgeoAlbersturns in longitude, moves up and downYes
albers usageoAlbersUsapansNo
azimuthal equal areageoAzimuthalEqualAreaturns in longitude and latitudeYes
azimuthal equidistantgeoAzimuthalEquidistantturns in longitude and latitudeYes
conic conformalgeoConicConformalturns in longitude and latitudeYes
conic equal areageoConicEqualAreaturns in longitude, moves up and downYes
conic equidistantgeoConicEquidistantturns in longitude, moves up and downYes
equal earthgeoEqualEarthturns in longitude, moves up and downYes
equirectangulargeoEquirectangularturns in longitude, moves up and downYes
gnomonicgeoGnomonicturns in longitude and latitudeYes
mercatorgeoMercatorturns in longitude, moves up and downYes
natural earthgeoNaturalEarth1turns in longitude, moves up and downYes
natural earth1geoNaturalEarth1turns in longitude, moves up and downYes
orthographicgeoOrthographicturns in longitude and latitudeYes
stereographicgeoStereographicturns in longitude and latitudeYes
transverse mercatorgeoTransverseMercatorturns in longitude and latitudeYes

In the lazy chunk ​

The projections of d3-geo-projection: one chunk for all of them.

projection.typed3 projectionA drag on a world mapfitbounds
airygeoAiryturns in longitude, moves up and downYes
aitoffgeoAitoffturns in longitude, moves up and downYes
augustgeoAugustturns in longitude, moves up and downYes
bakergeoBakerturns in longitude, moves up and downYes
bertin1953geoBertin1953turns in longitude, moves up and downYes
boggsgeoBoggsturns in longitude, moves up and downYes
bonnegeoBonneturns in longitude, moves up and downYes
bottomleygeoBottomleyturns in longitude, moves up and downYes
bromleygeoBromleyturns in longitude, moves up and downYes
collignongeoCollignonturns in longitude, moves up and downYes
craiggeoCraigturns in longitude, moves up and downNo
crastergeoCrasterturns in longitude, moves up and downYes
cylindrical equal areageoCylindricalEqualAreaturns in longitude, moves up and downYes
cylindrical stereographicgeoCylindricalStereographicturns in longitude, moves up and downYes
eckert1geoEckert1turns in longitude, moves up and downYes
eckert2geoEckert2turns in longitude, moves up and downYes
eckert3geoEckert3turns in longitude, moves up and downYes
eckert4geoEckert4turns in longitude, moves up and downYes
eckert5geoEckert5turns in longitude, moves up and downYes
eckert6geoEckert6turns in longitude, moves up and downYes
eisenlohrgeoEisenlohrturns in longitude, moves up and downYes
faheygeoFaheyturns in longitude, moves up and downYes
foucautgeoFoucautturns in longitude, moves up and downYes
foucaut sinusoidalgeoFoucautSinusoidalturns in longitude, moves up and downYes
ginzburg4geoGinzburg4turns in longitude, moves up and downYes
ginzburg5geoGinzburg5turns in longitude, moves up and downYes
ginzburg6geoGinzburg6turns in longitude, moves up and downYes
ginzburg8geoGinzburg8turns in longitude, moves up and downYes
ginzburg9geoGinzburg9turns in longitude, moves up and downYes
gringortengeoGringortenturns in longitude, moves up and downYes
gringorten quincuncialgeoGringortenQuincuncialturns in longitude, moves up and downYes
guyougeoGuyouturns in longitude, moves up and downYes
hammergeoHammerturns in longitude, moves up and downYes
hillgeoHillturns in longitude, moves up and downYes
homolosinegeoHomolosineturns in longitude, moves up and downYes
hufnagelgeoHufnagelturns in longitude, moves up and downYes
hyperellipticalgeoHyperellipticalturns in longitude, moves up and downYes
kavrayskiy7geoKavrayskiy7turns in longitude, moves up and downYes
lagrangegeoLagrangeturns in longitude, moves up and downYes
larriveegeoLarriveeturns in longitude, moves up and downYes
laskowskigeoLaskowskiturns in longitude, moves up and downYes
loximuthalgeoLoximuthalturns in longitude, moves up and downYes
millergeoMillerturns in longitude, moves up and downYes
mollweidegeoMollweideturns in longitude, moves up and downYes
mt flat polar parabolicgeoMtFlatPolarParabolicturns in longitude, moves up and downYes
mt flat polar quarticgeoMtFlatPolarQuarticturns in longitude, moves up and downYes
mt flat polar sinusoidalgeoMtFlatPolarSinusoidalturns in longitude, moves up and downYes
natural earth2geoNaturalEarth2turns in longitude, moves up and downYes
nell hammergeoNellHammerturns in longitude, moves up and downYes
nicolosigeoNicolositurns in longitude, moves up and downYes
pattersongeoPattersonturns in longitude, moves up and downYes
peirce quincuncialgeoPeirceQuincuncialturns in longitude, moves up and downNo
polyconicgeoPolyconicturns in longitude, moves up and downYes
rectangular polyconicgeoRectangularPolyconicturns in longitude, moves up and downYes
robinsongeoRobinsonturns in longitude, moves up and downYes
satellitegeoSatelliteturns in longitude, moves up and downNo
sinu mollweidegeoSinuMollweideturns in longitude, moves up and downYes
sinusoidalgeoSinusoidalturns in longitude, moves up and downYes
timesgeoTimesturns in longitude, moves up and downYes
van der grintengeoVanDerGrintenturns in longitude, moves up and downYes
van der grinten2geoVanDerGrinten2turns in longitude, moves up and downYes
van der grinten3geoVanDerGrinten3turns in longitude, moves up and downYes
van der grinten4geoVanDerGrinten4turns in longitude, moves up and downYes
wagner4geoWagner4turns in longitude, moves up and downYes
wagner6geoWagner6turns in longitude, moves up and downYes
wiechelgeoWiechelturns in longitude, moves up and downYes
winkel tripelgeoWinkel3turns in longitude, moves up and downYes
winkel3geoWinkel3turns in longitude, moves up and downYes

Holochart extra ​

globe3d is not a Plotly projection: it is Holochart's own type, for the 3D globe. Its view (the rotation, the scale, what a drag does and fitbounds) is that of orthographic.

projection.typeIts view is that ofA drag on a world mapfitbounds
globe3dorthographicturns in longitude and latitudeYes

The four regions above are generated from the tables D3_GEO_PROJECTIONS and D3_GEO_PROJECTION_PROJECTIONS and from GLOBE_PROJECTION of packages/traces-geo/src/geo/constants.ts by pnpm --filter @mk7s/holochart-docs gen:galleries (apps/docs/scripts/gen-galleries.ts), which the docs build runs too. Do not edit between the markers.

The 3D globe ​

projection.type: 'globe3d' draws the map as a lit sphere. It is the orthographic view in three dimensions: the same projection.rotation and projection.scale, the same drags, wheel and keys, the same relayout keys, and every point lands on the pixel the orthographic map puts it on. Switching a subplot between the two is a relayout of one attribute.

Loading example…
geo/globe3d

What the third dimension adds:

  • It turns for free. The base layers and the regions of a choropleth are meshes on a sphere, built once; a rotation or a zoom moves them with one matrix and projects nothing. A globe at resolution: 50 keeps its 50m coastlines while it turns (see Performance).

  • Lines are arcs. A scattergeo line rises above the surface along its great circle, higher the longer it is, and passes behind the globe where the route does. line.lift sets the height per radian of length, in globe radii (default 0.15; 0 keeps lines on the surface).

    Loading example…
    scattergeo/globe-arcs
  • Regions can stand up. A choropleth's elevation gives each location a second value, drawn as the height of a prism rising from the sphere; elevationscale is the height of the largest one in globe radii (default 0.25). Hover picks what is drawn: a prism is hit on its walls and its cap, and it hides the regions behind it.

    Loading example…
    choropleth/globe

line.lift, elevation and elevationscale are Holochart's own and are ignored on a flat map.

What to know:

  • Leave room for what rises. At projection.scale: 1 the globe fills its domain, and an arc or a prism that rises past the limb at the top or bottom is cut by the domain's edge. A scale under 1 (the examples use 0.85 and 0.9) leaves room.
  • The view is orthographic. There is no perspective, no tilt and no orbit camera: the globe always faces you.
  • Markers and text are drawn on top, as on a flat map. They hide on the far side, but a marker behind a prism is drawn over it.
  • The light is fixed: from the upper left, the same at every rotation, with no highlights, so a choropleth's colors stay close to its colorscale. There are no lighting attributes yet.
  • It loads more code, on first use: the globe's own chunk (12 kB) and the renderer's 3D meshes, lines and pickers (about 27 kB together, shared with 3D scenes). A flat map loads none of it.

At the edges of a map ​

A flat map has to end somewhere, and a globe hides its far side. d3-geo cuts every polygon and line where the map ends and closes the pieces, which is where map renderers usually show seams, slivers or fills that cover everything but the region. What to expect:

The antimeridian. A world map ends at the meridian opposite projection.rotation.lon. The base map keeps countries that lie on both sides of 180° in one piece (Russia, Fiji, the Aleutians), so on a map centered on the Pacific they are drawn whole. An axis range can cross 180° too: lonaxis.range: [100, -100] runs east from 100° E to 100° W.

Loading example…
geo/antimeridian

The poles. Antarctica is a polygon around a pole. On a flat map it is closed along the bottom edge; seen from below on an orthographic map it is an island like any other.

Loading example…
geo/poles

Holes and islands. A country that surrounds another has a hole in it (South Africa around Lesotho), and a country of several parts is a multipolygon; a choropleth fills exactly those. Large lakes are a layer of their own on top of the land (showlakes).

Loading example…
geo/holes

The limb of a globe. On an orthographic map everything is cut at the edge of the visible hemisphere: lines run to it and come back on the other side, filled areas are closed along it, and markers behind it are not drawn.

Loading example…
geo/globe-limb

Fitting the view ​

fitbounds fits the view to the data of the subplot's traces:

  • 'locations' (the default): to the points and regions the traces draw. A scattergeo trace adds room for half its largest marker; a choropleth is fitted to the bounds of its regions, and regions on both sides of the antimeridian fit as one group.
  • 'geojson': to the whole geojson of the traces, drawn or not.
  • false: the view is what the layout says: the ranges, the rotation, the center and the scale.

What the fit sets depends on the map: center and projection.scale on a scoped map; also projection.rotation.lon on a world map; and on a world map of a projection that shows part of the globe (orthographic, …) projection.rotation.lat and both axis ranges as well.

The fit is off when the figure sets any of the attributes it would set: a figure that gives center, projection.scale, a rotation or an axis range wants that view. It is also off for the projections it cannot fit: albers usa, craig, peirce quincuncial and satellite.

When the user moves the map, the gesture's relayout carries geo.fitbounds: false, so the next draw does not fit again over what the user chose.

Interactions ​

Hover, click and selection belong to the traces: see scattergeo and choropleth. The subplot itself is moved with the pointer, when layout.dragmode is 'pan' or 'zoom' (either: a drag never draws a zoom box on a map):

GestureWhat it does
Drag, one fingerMoves the map: see the table below
WheelZooms about the pointer, when config.scrollZoom allows 'geo' (the default does)
PinchZooms about the middle of the two fingers
Double-clickGoes back to the first drawn view, and emits doubleclick
Press and releaseA click on the points under the pointer

With dragmode: 'select' or 'lasso' a drag selects, the map does not move, and the wheel scrolls the page. config.staticPlot turns all of it off.

What a drag does, and what the relayout that ends a gesture carries, depends on the kind of map. The keys are Plotly's, prefixed with the subplot's id (geo2.… for the second map):

MapA dragKeys of the relayout
Scoped: any scope but world, and albers usaPans. A zoom scales about the pointer.geo.center.lon, geo.center.lat, geo.projection.scale
World, whole sphere: equirectangular, natural earth, mercator, …Turns the map in longitude, so that the meridian that was grabbed stays under the pointer, and moves it up and down.geo.projection.rotation.lon, geo.center.lon, geo.center.lat, geo.projection.scale
World, part of the sphere: orthographic, stereographic, …Turns the globe in longitude and latitude, so that the point that was grabbed stays under the pointer. A zoom scales about the middle.geo.projection.rotation.lon, geo.projection.rotation.lat, geo.projection.scale

The projection table above says which world maps are of the third kind ("turns in longitude and latitude").

While a gesture is under way the chart emits relayouting with the same keys; when the pointer is released, or the wheel has rested, one relayout commits the view. A key whose value did not change is left out, and geo.fitbounds: false is added when fitbounds was on. projection.minscale and projection.maxscale limit the zoom.

ts
chart.on('relayout', (update) => {
  const lon = update['geo.projection.rotation.lon'];
  if (lon !== undefined) console.log('turned to', lon);
});

// The same keys set the view from code.
void chart.relayout({ 'geo.projection.rotation.lon': -100, 'geo.projection.scale': 2 });

layout.uirevision keeps the user's view across react calls, as it keeps any other edit made by hand. The subplot's own geo.uirevision is accepted but not read yet.

Keyboard ​

With the focus in the plot area, the arrow keys step through the points and regions of the geo traces (see the accessibility guide), and the view keys of the 3D scenes move the map:

KeysWhat they do
Shift + arrowsMove the view where the arrow points, by a tenth of the subplot: a scoped map pans, a world map turns, as under a drag
+ / -Zoom in and out about the middle of the map, by a factor of 1.25, inside projection.minscale and projection.maxscale
0Go back to the first drawn view, like a double-click

Each key press is one relayout with the same keys as the gesture it stands for, and the new view is announced (the longitude and latitude in the middle of the map, and the scale). With the cursor on a point or a region, its subplot gets the keys; without a cursor, every geo subplot does.

There are no modebar buttons for maps yet.

Several maps and other subplots ​

A figure can hold several geo subplots, geo, geo2, geo3, …, each with its own projection, view and base layers. A trace picks one with geo: 'geo2'. Place them with domain: either fractions of the plot area (domain.x, domain.y) or a cell of layout.grid (domain.row, domain.column). Subplots without a domain are stacked, the first at the bottom.

A map keeps its shape, so it fills the width or the height of its domain, rarely both.

ts
import { createChart } from '@mk7s/holochart';
import '@mk7s/holochart/geo';

// Two maps side by side in a grid: the same data on a flat map and on a globe.
const cities = { lon: [-74, 2.35, 139.7, 151.2], lat: [40.7, 48.86, 35.7, -33.9] };

createChart(document.getElementById('chart')!, {
  data: [
    { type: 'scattergeo', ...cities, geo: 'geo' },
    { type: 'scattergeo', ...cities, geo: 'geo2' },
  ],
  layout: {
    grid: { rows: 1, columns: 2 },
    geo: { domain: { row: 0, column: 0 }, fitbounds: false },
    geo2: {
      domain: { row: 0, column: 1 },
      fitbounds: false,
      projection: { type: 'orthographic', rotation: { lon: 60, lat: 20 } },
    },
  },
});

A map sits next to cartesian axes, polar subplots or 3D scenes the same way: give each its domain. Here a choropleth takes the left of the figure and a bar chart of the same values the right:

ts
import { createChart } from '@mk7s/holochart';
import '@mk7s/holochart/geo';

const codes = ['BRA', 'ARG', 'COL', 'PER'];
const values = [216, 46, 52, 34];

createChart(document.getElementById('chart')!, {
  data: [
    { type: 'choropleth', locations: codes, z: values, showscale: false },
    { type: 'bar', x: values, y: codes, orientation: 'h' },
  ],
  layout: {
    geo: { domain: { x: [0, 0.55] }, scope: 'south america' },
    xaxis: { domain: [0.65, 1] },
  },
});

Each geo subplot draws in a viewport of its own, clipped to the box of its ranges, so a map never paints over its neighbours.

What the data costs ​

The base map is data, and data is most of what a map downloads. All of it is in the geo package as lazy chunks, loaded with import() the first time a subplot needs them: your bundler emits them as files next to your own chunks, and a page that draws no map loads none of them.

ChunkLoaded whenSize, min + gzip
Base, 110m: countries and landA map at resolution: 110 (the default) is drawn31.3 kB
Extras, 110m: lakes, rivers, US statesThat map shows lakes, rivers or state borders, or a trace uses locationmode: 'USA-states'8.7 kB
Base, 50mA map at resolution: 50 is drawn178.3 kB
Extras, 50m: lakes, rivers, states and provincesAs for 110m104.9 kB
Country namesA trace uses locationmode: 'country names'a 38 kB table before compression
Projections of d3-geo-projectionA figure names one of themabout 12 kB

So a map at the default resolution downloads about 40 kB of data with every layer, and a map at resolution: 50 about 283 kB. Coastlines, borders and the ocean are derived from the countries and cost nothing more. The default template shows lakes, so with it a map loads both chunks of its resolution; set showlakes: false (and no rivers or state borders) to load the base chunk alone.

resolution: 110 is drawn from Natural Earth's 1:110,000,000 data: 176 countries, without the smallest. It suits world maps. resolution: 50 (1:50,000,000) has 238 countries and finer coasts, which a map of one continent or country shows.

A chunk that fails to load, or takes longer than 30 seconds, never holds chart.ready: the map is drawn without those layers, traces that needed the data draw nothing, and the console says what was missing, once.

Performance ​

Everything on a map is projected on the CPU through d3-geo and drawn by the same GPU primitives as the other charts. What that costs depends on the gesture:

  • A pan or a zoom is free. Projected geometry is kept in pixels, and a pan or a zoom only changes a transform, whatever the amount of data. When a zoom has settled, curves are projected once more at the new scale, so that they stay smooth.
  • A rotation projects again, on every frame: the base layers and every trace on the subplot. At resolution: 110 a world's base layers fit in a frame on the machine they were measured on (below), with a few milliseconds left for the traces.
  • A 3D globe is the exception: it turns by a matrix and projects nothing, at any resolution and with any number of regions. On an M1 Max a 50m globe with every layer turns in about 9 ms a frame, nearly all of it drawing; the same map as orthographic would take 92 ms a frame at 50m, which is why flat maps do the following.
  • A flat map at resolution: 50 shows 110m coastlines while it rotates. The 50m data cannot be projected in a frame (about 90 ms measured for a world on an M1 Max, against 12 ms for 110m), so during a rotation the base layers, and choropleth regions matched against them, are drawn from the 110m data. When the rotation ends the 50m layers come back over a few frames. On a world view the difference is hard to see; zoomed in, coasts visibly sharpen when you let go.
  • Your own GeoJSON has no coarser copy and is projected in full while it rotates. Measured on an M1 Max, a choropleth of a trace's own geojson turns at 60 fps up to about 25,000 vertices and slows down above that. Simplify large files, or put them on a scoped map, which only pans and zooms.
  • Scatter traces project their points again on each frame of a rotation: 100,000 markers take 6 to 13 ms on the same machine. Long lines cost more and are not simplified.

These figures are from one fast laptop. Slower machines have not been measured, and on them a turning world map may miss 60 fps even at 110m.

The performance guide covers the rest of a chart.

Offline use and CSP ​

By default a map makes no network request of its own: the base map and the country names are chunks of your app's bundle, loaded from your own origin. Maps therefore work offline, and under a Content Security Policy they need nothing beyond what any chart needs (script-src 'self' covers the chunks). See the CSP guide.

Two attributes fetch from a URL you give, and each needs its origin in connect-src:

  • config.topojsonURL: the base map is fetched from there instead of the bundle;
  • a trace's geojson given as a URL instead of an object.

Data, attribution and boundaries ​

The base map is built from Natural Earth 5.1.2 vector data at 1:110m and 1:50m: countries, land, lakes, rivers, and states and provinces. Natural Earth is in the public domain and asks for no credit; Holochart credits it here and in the package's THIRD_PARTY_NOTICES.md, and draws no attribution on the map. The table behind locationmode: 'country names' is the one plotly.js uses, with its own notices in the same file.

Boundaries are political. Natural Earth draws disputed borders by de facto control: the line shown is where each side administers, which is not what every government or reader accepts. Kosovo is a country of the base map, under the user-assigned code XKX. If your audience or your organization needs other boundaries, replace them:

  • The whole base map: set config.topojsonURL to a folder you serve. The files are then fetched from it, in Plotly's layout and under Plotly's names, one per scope and resolution: <url>/world_110m.json, <url>/europe_50m.json, <url>/north-america_110m.json, …. Each is a TopoJSON topology with the objects coastlines, land, ocean, lakes, rivers, countries and subunits; countries are matched to locations by their id. Files made for plotly.js work as they are. This is also how to get finer data than 1:50m.

    ts
    import { createChart } from '@mk7s/holochart';
    import '@mk7s/holochart/geo';
    
    createChart(document.getElementById('chart')!, {
      data: [{ type: 'choropleth', locations: ['FRA', 'DEU'], z: [68, 84] }],
      config: { topojsonURL: '/maps/' },
    });
  • The regions of one trace: give the trace its own geojson and match it with featureidkey (choropleth). With geo.visible: false the base map is not drawn at all.

Default maps are not pixel-identical to Plotly's: plotly.js now fetches files that draw United Nations boundaries, and Holochart ships Natural Earth.

Plotly compatibility ​

layout.geo has every attribute of Plotly's, with the same names and defaults (those of plotly.js 4.1.1, where fitbounds defaults to 'locations'). The differences:

  • The package is not in the full bundle: a figure needs import '@mk7s/holochart/geo'.
  • No request by default. config.topojsonURL is empty by default and the bundled data is used; Plotly defaults to its CDN.
  • Scopes are picked out of one world file. A country polygon that straddles a scope's box stays whole; Plotly's scope files are cut at the box.
  • dragmode: 'zoom' moves the map, as 'pan' does. Plotly moves a map in 'pan' mode only, and turns 'zoom' into 'pan' for figures that have nothing but maps.
  • geo.uirevision is not read; layout.uirevision is.
  • fullLayout.geo is complete. Where Plotly leaves an attribute out because the map cannot use it, the defaulted container has the value that draws the same map (a layer that cannot be shown is false, an Albers USA map has a rotation of zero).
  • The graticule keeps its last meridian. Plotly drops the last meridian of every map (the duplicate at 180° on a world map with default ticks, but also 150° E on scope: 'asia' and 60° E on 'europe'); Holochart drops it only when it is a full turn after the first. A dtick that is not a positive number draws no lines.
  • scope: 'usa' with another projection than Albers USA is drawn, turned to the middle of its range; Plotly throws.
  • Modebar buttons for maps (zoom in, zoom out, reset) are not there yet. The view keys are Holochart's own.
  • Tile maps (layout.map, scattermap, choroplethmap, densitymap) are not available.

The trace pages list what differs per trace: scattergeo and choropleth.

Released under the MIT License.