luma.gl (MIT, 9.4.2, github.com/visgl/luma.gl (https://github.com/visgl/luma.gl 2,483 ), about 2,500 stars) is a GPU toolkit with WebGL 2 and WebGPU back ends behind one API of devices, buffers, textures and models. Most developers meet it through deck.gl 492,297 (MIT, 9.4.0, github.com/visgl/deck.gl (https://github.com/visgl/deck.gl 14,614 ), about 14,600 stars), the large-data visualization framework begun at Uber and now part of vis.gl, an OpenJS Foundation project. You describe layers (points, arcs, hexagon bins, columns, GeoJSON, 3D tiles) over data arrays, and deck.gl builds the instanced buffers and shaders, handles picking and animates transitions. Its standalone bundle draws BookNest's ratings as 3D columns:
<div id="chart" style="position:relative;width:600px;height:190px;background:#ece8de"></div>
<script src="https://cdn.jsdelivr.net/npm/deck.gl@9.4.0/dist.min.js"></script>
<script src="webgl-helper.js"></script>
<script>
const books = [4.6, 4.3, 4.8, 4.1, 4.5, 4.4].map((rating, i) =>
({ rating, i, color: GLH.rgb(GLH.covers[i]).map((c) => c * 255) }));
new deck.Deck({
parent: document.getElementById('chart'), width: 600, height: 190,
views: new deck.OrbitView({ fovy: 30 }), // no map: plain 3D coordinates
initialViewState: { target: [0, 0, 22], rotationX: 12, rotationOrbit: -10, zoom: 1.5 },
layers: [new deck.ColumnLayer({
id: 'ratings', data: books, coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
getPosition: (d) => [(d.i - 2.5) * 30, 0], getElevation: (d) => (d.rating - 3.5) * 40,
getFillColor: (d) => d.color, radius: 10, diskResolution: 24, extruded: true,
})],
onAfterRender: () => { window.__done = true; },
});
</script>
Heights are ratings above 3.5 stars, so Salt and Saffron's 4.8 stands tallest. With a MapView and longitudes and latitudes, the same layer stands on a map, usually over MapLibre 34,367 (MapLibre GL for Vector Maps). The page logged two luma.gl warnings about uniform-block reflection falling back to its own std140 layout (UBO Layout), then rendered correctly.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
#chart { position: relative; width: 100%; max-width: 600px; height: 300px; background: #ece8de; }
.names { display: flex; max-width: 600px; text-align: center; font: 10px monospace; }
.names div { flex: 1; padding: 3px 1px; }
p { max-width: 600px; margin: 2px 6px; font: 11px monospace; }
</style>
<div id="chart"></div>
<div class="names" id="names"></div>
<p id="out"></p>
<!-- 9.3.11 rather than 9.4.0: 9.4.0 lost its WebGL context about 2 s in on the test GPU -->
<script src="https://cdn.jsdelivr.net/npm/deck.gl@9.3.11/dist.min.js"></script>
<script>
const books = [['The Quiet Harbor', 4.6, '#1e5f8c'], ['Small Steps', 4.3, '#5b3f99'], ["Clockmaker's Paradox", 4.1, '#e09a10'],
['Salt and Saffron', 4.8, '#3f7d3a'], ['Quiet Harbor (audio)', 4.5, '#b5452e'], ['Saffron (2nd ed.)', 4.4, '#2a9d8f']]
.map(([title, rating, hex], i) => ({ title, rating, i, color: [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16)) }));
document.getElementById('names').innerHTML = books.map((b) => `<div>${b.title}<br><b>${b.rating}</b></div>`).join('');
new deck.Deck({
parent: document.getElementById('chart'), width: '100%', height: 300,
views: new deck.OrbitView({ fovy: 30 }), // no map: plain 3D coordinates
initialViewState: { target: [0, 0, 20], rotationX: 14, rotationOrbit: -12, zoom: 1.6 },
controller: true, // drag to orbit
layers: [
new deck.ColumnLayer({
id: 'ratings', data: books, coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
getPosition: (d) => [(d.i - 2.5) * 30, 0], getElevation: (d) => (d.rating - 3.5) * 40,
getFillColor: (d) => d.color, radius: 10, diskResolution: 24, extruded: true,
pickable: true, autoHighlight: true,
}),
new deck.ColumnLayer({ // a flat floor under the columns
id: 'floor', data: [0], coordinateSystem: deck.COORDINATE_SYSTEM.CARTESIAN,
getPosition: () => [0, 0], getElevation: 0.5, getFillColor: [200, 192, 178], radius: 110,
diskResolution: 4, angle: 45, extruded: true,
}),
],
getTooltip: ({ object }) => object?.title && `${object.title}: ${object.rating} stars`,
onLoad: () => { document.getElementById('out').textContent = `deck.gl ${deck.VERSION} | heights are ratings above 3.5 stars`; },
});
</script>