chlobe: a 15KB React choropleth version of the brilliant cobe.
pnpm add @lachlanjc/chlobeGlobe: logarithmic color scale · bars: annual production
Darker dots, more forest per person · logarithmic scale
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.
Requires React 18+ and WebGL:
pnpm add @lachlanjc/chlobeRender a globe with an RGB color ramp (it’s more efficient for shaders):
'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], }}/>Hover or focus a legend button to highlight its country. Click it to rotate the globe there.
'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> ))} </> );};Hover a country to see its name and renewable electricity share. Countries outside the dataset show “No data”.
'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> )}/>| Property | Default | Description |
|---|---|---|
| data | Required | Country entries: alpha2, value, optional id and label · IDs default to alpha2 |
| colors | Required | Two filled RGB tuples and one missing tuple; channels 0–255 |
| colors.missingAlpha | 0.25 | Missing-country opacity |
| size | Container width | Square size in CSS pixels |
| colorScheme | light | light or dark |
| activeEntryId | Uncontrolled | Selected ID; null clears · Pair with onActiveEntryChange |
| defaultActiveEntryId | null | Initial uncontrolled selection |
| onCountryHover | Hovered country code or null | |
| formatValue | Formats (value, entry) | |
| renderTooltip | Your tooltip UI; absent by default | |
| onError | WebGL initialization or context-loss errors | |
| aria-label | Country choropleth globe | Describe your data |
| className / style | Applied to the container |
| Property | Default | Description |
|---|---|---|
| autoRotate | true | Idle rotation |
| interactive | true | Dragging, hover, and left/right arrows |
| initialPhi | 0 | Starting rotation in radians |
| baseColor / glowColor | Color scheme | Surface and glow RGB tuples |
A static image needs both interactive: false and autoRotate: false.
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.
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.
Include an aria-label with a text summary. Interactive globes support keyboard rotation; static globes use an image role. Reduced motion stops idle rotation.