chlobe

Visualize country data on a glbe.

chlobe: a 15KB React version of the brilliant cobe.

pnpm add @lachlanjc/chlobe

Renewable electricity

0%100% renewable
Renewable electricityOther generation

Oil production

Less productionMore production

Globe: logarithmic color scale · bars: annual production

Selected producersAnnual production

Forest area per person

  • 8.66 haCanada
  • 5.67 haRussian Federation
  • 2.33 haBrazil
  • 1.16 haCongo, Dem. Rep.
  • 0.16 haChina
  • 0.05 haIndia
0 ha/person24.11 ha/person

Darker dots, more forest per person · logarithmic scale

Freshwater withdrawals
per person

0 m³/person3,630 m³/person· square-root globe scale

The story

Most globes on the web use 3D renderers, which are massive. Shu Ding’s cobe realized you could prebuild a tiny bitmap of where land is and render a sphere with a shader in just 5KB. Brilliant!

But cobe doesn’t know where countries are. Adding GeoJSON data and rendering it is easily >100KB. chlobe evolves cobe’s approach, preprocessing the GeoJSON and uniquely coloring each country into the compressed bitmap, then keeps an index of country codes & colors to render levels over each country’s dots using a provided color scale. The library is just 15KB minified.

Thanks to Nick Rabinowitz for this idea.

Install & use

Requires React 18+ and WebGL:

pnpm add @lachlanjc/chlobe

Render a globe with an RGB color ramp (it’s more efficient for shaders):

globe.tsx
'use client';import { ChoroplethGlobe } from '@lachlanjc/chlobe';// Renewable electricity share (%), 2021.const data = [  { alpha2: 'NO', value: 99.1 },  { alpha2: 'BR', value: 77.4 },  { alpha2: 'DE', value: 39.8 },  { alpha2: 'CN', value: 28.4 },  { alpha2: 'SA', value: 0.1 },];<ChoroplethGlobe  aria-label="Example renewable electricity shares by country"  colors={{    filled: [[219, 234, 254], [29, 78, 216]],    missing: [148, 163, 184]  }}  data={data}  globe={{    autoRotate: true,    interactive: true,    baseColor: [255, 255, 255],    glowColor: [217, 217, 230],  }}/>
Options

Custom legend

Hover or focus a legend button to highlight its country. Click it to rotate the globe there.

globe-legend.tsx
'use client';import { useRef } from 'react';import { ChoroplethGlobe, type ChoroplethGlobeRef } from '@lachlanjc/chlobe';// Renewable electricity share (%), 2021.const data = [  { alpha2: 'NO', value: 99.1 },  { alpha2: 'BR', value: 77.4 },  { alpha2: 'DE', value: 39.8 },  { alpha2: 'CN', value: 28.4 },  { alpha2: 'SA', value: 0.1 },];const countryNames = new Intl.DisplayNames(['en'], { type: 'region' });export const GlobeWithLegend = () => {  const globe = useRef<ChoroplethGlobeRef>(null);  return (    <>      <ChoroplethGlobe        aria-label="Custom legend globe preview"        ref={globe}        data={data}        globe={{          autoRotate: true,          interactive: true,          baseColor: [255, 255, 255],          glowColor: [217, 217, 230],        }}        colors={{          filled: [[219, 234, 254], [29, 78, 216]],          missing: [148, 163, 184]        }}      />      {data.map(({ alpha2, value }) => (        <button          key={alpha2}          type="button"          onMouseEnter={() => globe.current?.hoverEntry(alpha2)}          onMouseLeave={() => globe.current?.clearHoveredEntry()}          onFocus={() => globe.current?.hoverEntry(alpha2)}          onBlur={() => globe.current?.clearHoveredEntry()}          onClick={() => globe.current?.navigateToEntry(alpha2)}        >          {countryNames.of(alpha2) ?? alpha2} · {value}%        </button>      ))}    </>  );};
Options

Custom tooltip

Hover a country to see its name and renewable electricity share. Countries outside the dataset show “No data”.

globe-tooltip.tsx
'use client';import { ChoroplethGlobe } from '@lachlanjc/chlobe';// Renewable electricity share (%), 2021.const data = [  { alpha2: 'NO', value: 99.1 },  { alpha2: 'BR', value: 77.4 },  { alpha2: 'DE', value: 39.8 },  { alpha2: 'CN', value: 28.4 },  { alpha2: 'SA', value: 0.1 },];const countryNames = new Intl.DisplayNames(['en'], { type: 'region' });<ChoroplethGlobe  aria-label="Country tooltip globe preview"  data={data}  colors={{    filled: [[219, 234, 254], [29, 78, 216]],    missing: [148, 163, 184]  }}  globe={{    autoRotate: true,    interactive: true,    baseColor: [255, 255, 255],    glowColor: [217, 217, 230],  }}  formatValue={(value) => `${value}% renewable electricity`}  renderTooltip={({ alpha2, label, formattedValue, x, y }) => (    <div      role="tooltip"      style={{        position: 'absolute',        left: 0,        top: 0,        transform: `translate(${x}px, ${y}px) translate(-50%, calc(-100% - 12px))`,        pointerEvents: 'none',        whiteSpace: 'nowrap',        background: '#ffffff',        color: '#354150',        border: '1px solid #cbd5e1',        borderRadius: 2,        boxShadow: '0 4px 16px rgb(15 23 42 / 0.12)',        fontSize: 13,        padding: '8px 10px',      }}    >      <strong>{label ?? countryNames.of(alpha2) ?? alpha2}</strong>      <span style={{ display: 'block' }}>        {formattedValue ?? 'No data'}      </span>    </div>  )}/>
Options

API reference

PropertyDefaultDescription
dataRequiredCountry entries: alpha2, value, optional id and label · IDs default to alpha2
colorsRequiredTwo filled RGB tuples and one missing tuple; channels 0–255
colors.missingAlpha0.25Missing-country opacity
sizeContainer widthSquare size in CSS pixels
colorSchemelightlight or dark
activeEntryIdUncontrolledSelected ID; null clears · Pair with onActiveEntryChange
defaultActiveEntryIdnullInitial uncontrolled selection
onCountryHoverHovered country code or null
formatValueFormats (value, entry)
renderTooltipYour tooltip UI; absent by default
onErrorWebGL initialization or context-loss errors
aria-labelCountry choropleth globeDescribe your data
className / styleApplied to the container

Globe options

PropertyDefaultDescription
autoRotatetrueIdle rotation
interactivetrueDragging, hover, and left/right arrows
initialPhi0Starting rotation in radians
baseColor / glowColorColor schemeSurface and glow RGB tuples

A static image needs both interactive: false and autoRotate: false.

Colors

Colors range from the smallest to largest absolute value among supported countries in your data. Values of 10 and −10 get the same color. For a logarithmic ramp with nonnegative values, pass Math.log1p(value) as each entry’s value. Keep the original values for tooltip formatting.

Selection and tooltips

Active entries rotate into view and pause idle rotation. Ref methods: hoverEntry(id) selects, clearHoveredEntry() clears, and navigateToEntry(id) only rotates.

Tooltips receive { source, alpha2, entry, entryId, label, formattedValue, x, y }. The x and y coordinates position your tooltip in CSS pixels relative to the globe. Countries outside your data have no entry or formatted value. Provide formatValue to format values for display.

Accessibility

Include an aria-label with a text summary. Interactive globes support keyboard rotation; static globes use an image role. Reduced motion stops idle rotation.