cosmos.gl: a force graph where the physics and the drawing both run in shaders
GPU-accelerated force graph layout and rendering
At a glance
- What is it?
- cosmos.gl is a MIT-licensed TypeScript library that computes force layout and rendering entirely in WebGL 2 fragment and vertex shaders, which is what lets it hold hundreds of thousands of points. It is ESM-only, needs Node 22, and its v3 config API has a genuine trap: setConfig() resets every value to defaults before applying yours.
- Who is it for?
- Adopt cosmos.gl when you are rendering a graph large enough that a CPU layout is the bottleneck, and when you control the frontend build so an ESM-only package and a Node 22 floor are not problems. Do not adopt it to replace a small graph visualisation, where the WebGL 2 requirement and the absence of a canvas fallback buy you nothing, and do not adopt it expecting d3-force semantics, because d3 appears in the dependency tree only as type definitions and a colour scale.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
setConfig() wipes your config, and that is the v3 trap
One line in the v3.0 change list is the most consequential thing in the README, and it is easy to read past because it is filed under improvements.
setConfig() now resets all values to defaults before applying. There is a new setConfigPartial() to update individual properties without resetting the rest.
In a version where most state moved from methods to config properties, this is a real hazard. v3.0 replaced the imperative selection methods with config properties: highlightedPointIndices, highlightedLinkIndices and outlinedPointIndices, with points and links highlighted independently and focused links rendered wider through focusedLinkIndex and focusedLinkWidthIncrease. So the intended way to highlight something is now to write to the config. The intended way to write to the config is now setConfig(). And setConfig() silently discards every property you did not include in the call.
The failure is quiet. Your call to highlight a point resets spaceSize, resets the simulation parameters, drops your onClick handler, turns off enableDrag and resets fitViewOnInit. Nothing throws. The graph keeps rendering, the highlight appears, and everything else you configured at startup is gone. An application that sets its configuration once in the constructor and then updates selection through setConfig will appear to work in a demo and break in production.
The fix is one method name, and it is worth internalising before you write the first call: setConfigPartial() for anything after construction. Two related rules come with it. enableSimulation is changeable at runtime through either method, so the simulation toggle is no longer fixed at startup. And initialZoomLevel, randomSeed and attribution are init-only, preserved across config updates, which means a setConfig() call cannot accidentally re-randomise your layout even though it resets everything else.
This is the change a v2 user is most likely to hit, which is why the README points at migration-notes.md for upgrading rather than treating the list as informational.
The physics is not d3-force, and d3 is not even a runtime dependency
The fastest way to understand what cosmos.gl is, and what it is not, is to look at what is missing from the dependency tree.
The devDependencies list is full of d3 packages: @types/d3-array, @types/d3-color, @types/d3-drag, @types/d3-ease, @types/d3-scale, @types/d3-scale-chromatic, @types/d3-selection, @types/d3-transition and @types/d3-zoom, plus d3-scale-chromatic itself. Every one of those is a type definition package, and the one real package is a colour scale. There is no d3-force, and no runtime dependency on d3 at all in what is listed.
That is the whole design in one observation. The force layout is a custom implementation written in GLSL and executed on the GPU. The README states it directly: all the computations and drawing occur on the GPU in fragment and vertex shaders, avoiding expensive memory operations, which is what enables real-time simulation of network graphs consisting of hundreds of thousands of points and links on modern hardware.
This has a consequence that is easy to miss when you come from d3. You cannot reach for d3-force to compute a layout and hand the result in, because there is no d3-force here to swap. What you can do is the opposite: since rendering and simulation are now separate concerns, you can compute positions anywhere you like, including on a server, and ship the results. The v2.0 change list already moved in that direction, replacing setData with setPointPositions and setLinks.
The d3 type packages are there for the ecosystem conventions they make available in the storybook examples: scales, colour schemes, zoom and transition behaviour that a developer would otherwise reimplement to write a demo. The library itself asks for two typed arrays and a Float32Array of indices, and that is the entire data contract.
ESM only, Node 22 minimum, WebGL 2 with no fallback described
Three environment constraints, all checkable in package.json or the change list, and any one of them can rule the library out for a given project.
First, the module system. package.json sets type to module, and the build script runs vite twice, once with --mode es and once with --mode umd. The UMD bundle exists, but a UMD build does not make an ESM-first package requireable from CommonJS, because the type field is what decides how Node interprets the entry point. A CommonJS application that does not run a bundler cannot require() this package.
Second, the toolchain floor:
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0"
},Node 22 and npm 10, declared rather than implied. That is a recent floor and it applies to your build, not to the browser, since the output is a browser bundle.
Third, and the one that actually decides fit, the graphics requirement. v3.0 ported rendering from regl to luma.gl, with the parenthetical WebGL 2 in the change list. There is no WebGL 1 path and no canvas fallback described anywhere in the documentation. A WebGL 2 requirement means this does not run on a browser without it, and it does not run in a headless environment without a software GL implementation, which in practice means server-side rendering, static screenshots in CI and some test harnesses are all out of reach without extra work.
The package entry points reflect the dual build. main is dist/index.js, the jsdelivr field points at dist/index.min.js for direct browser loading, and types is dist/index.d.ts. The files array ships only dist and CITATION.cff, so the package is small, and the prepare script, sh scripts/prepare.sh, is what runs when you install from a git URL rather than from the registry.
Node 22 and WebGL 2 are not demanding by 2026 standards. They are just the two constraints a reader has to clear before reading anything else, and neither is discoverable from the quick start.
Async init and a Device you can share across graphs
The constructor returns before the graph is usable, which changes how you write the code around it, and v3 gives you two ways to deal with that.
Async initialization means the constructor returns immediately and all public methods queue until the device is ready. To know when the graph is usable, you use graph.ready or graph.isReady. In practice that means your first setPointPositions and setLinks calls are legal immediately and simply wait, but any logic that depends on the simulation having produced a layout needs the readiness signal rather than a timeout.
The second mechanism is device sharing. Rendering is ported to luma.gl, and you can share a custom Device across multiple graphs through a third constructor argument, new Graph(div, config, devicePromise). The argument is a promise, which fits the async model rather than sitting awkwardly beside it.
This matters for the dashboard case rather than the single-graph case. If you are putting four related graphs on one page, four separate Graph instances means four WebGL devices, four shader compilations and four contexts. Passing a device promise to each one amortises that. The README documents the signature and nothing else about the lifecycle, so the questions a reader has left are whether the caller owns the device, whether destroying one graph affects the others, and whether the promise is resolved once and shared or awaited per graph. Those are the things to check in the source or the storybook before committing to the pattern.
The rest of the interaction model in v3 is worth noting because it was rebuilt around input. Touch and pen support means tap, drag and pinch-to-zoom on phones and tablets, with a long-press of roughly 500 ms firing the context menu callbacks, and onMouseMove plus all click callbacks fire from touch input rather than requiring a separate path. Context menus have their own callback set, onContextMenu, onPointContextMenu, onLinkContextMenu and onBackgroundContextMenu, which is the difference between a graph you can attach actions to and one you can only click. Hover detection skips GPU work when the mouse has not moved, and hover now reports isHighlighted and isOutlined, and correctly resolves the topmost point when points overlap, which is a small correctness detail that matters as soon as your data has duplicates at the same coordinate.
Simulation and rendering are now separate, and that is the architectural win
The v3 change that reshapes how you use the library is not in the graphics list. It is that rendering is now separate from simulation.
The simulation starts automatically by default, and you control it independently with start(), stop(), pause() and unpause(). Separately, enableSimulation is a config property that can be changed at runtime through setConfig() or setConfigPartial().
That combination, an inert graph with no running physics, is what makes a lot of otherwise impractical use cases practical. The quick start configuration already demonstrates the intent, with simulationFriction at 0.1 described as keeping the graph inert and simulationGravity at 0 disabling the gravity force. Add enableSimulation false and the graph will draw positions you supply and never move them.
The important consequence is that layout can happen anywhere and rendering need not. A server can run the simulation once over a large graph and persist the coordinates; a static build can ship a precomputed layout; a report generator can produce a fixed arrangement that does not re-randomise on every page load. In each case the client does no physics work at all and the library becomes a high-performance renderer rather than a simulation engine. That is a different product from what v2 offered, and it is the reason the change is worth more than its bullet.
Two other performance controls sit in the same space. linkBlending set to false disables alpha blending for significantly faster link rendering on dense graphs, which is the knob you reach for when a hairball graph is too slow to interact with. And the collision force, new in v3, is GPU-accelerated and uses a spatial-hash grid that scales to large graphs, enabled with simulationCollision and tuned with simulationCollisionRadius for a fixed radius versus a size-derived one, and simulationCollisionPadding for the extra gap between points. Collision detection is normally the step that forces a layout back onto the CPU, so having it on the GPU is what makes non-overlapping large graphs feasible.
The GPU transition work supports the same idea from the rendering side. Point positions, point colours and sizes, and link colours and widths animate by default with a transitionDuration of 800 and TransitionEasing.CubicInOut, and onTransitionStart, onTransition and onTransitionEnd expose the lifecycle. Setting transitionDuration to 0 keeps snap updates, which is what you want when positions are arriving from a server rather than from a simulation.
package.json is ahead of the tags, and the repository URL is a dead protocol
Two metadata problems in package.json, both of the kind that cost a consumer an hour rather than a day.
The first is the version. package.json reads 3.4.1. The newest release tag is v3.4.0, dated 2026-07-27, preceded by v3.3.0 on 2026-07-12 and v3.2.0 on 2026-07-09. The last push was on 2026-09-18, so the default branch is ahead of the newest tag by both a patch version and roughly two months of commits. Anyone pinning to a release gets v3.4.0; anyone installing from a git URL or building the branch gets 3.4.1. The difference between those two artefacts is not documented, and the changelog in the repository is organised as What is New in v3.0 and What is New in v2.0 rather than as a per-patch record, so there is no quick way to find out what 3.4.1 changed.
The second is the repository URL:
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"repository": "git://github.com/cosmosgl/graph.git",
"type": "module",That repository field uses the git protocol, git://, which GitHub no longer serves. GitHub disabled the unencrypted git protocol in 2022, and a URL in this form fails at the transport layer with an error that does not name the cause. For a consumer installing from a git URL, this field is documentation, so the failure lands somewhere other than where the problem is. The correct form is an https URL. The npm registry link in the README works regardless, which is why the defect is easy to miss.
Neither issue affects the published artefact from the registry, and both are the kind of thing that a fix commits on the default branch resolve. They are worth reporting because an evaluator reading package.json to decide whether to depend on something is reading exactly this file.
The licence itself is MIT, and the file at the root is named LICENCE rather than LICENSE, which is a naming inconsistency worth knowing about if you have an automated check that looks for the American spelling.
A rendering engine with a foundation's worth of governance files
The repository listing contains more governance artefacts than most libraries of this size have source directories, and that is a signal about how the project intends to be governed.
There is CHARTER.md, GOVERNANCE.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md and CITATION.cff at the root. CITATION.cff is not only in the repository, it is in the package files array alongside dist, so anyone who installs the library receives the citation metadata and can cite it. That is a small decision with a large implication about intent: this is a project that expects to be depended on and referenced in academic and technical writing for years.
There is also a history/ directory and a migration-notes.md at the root, so the upgrade path and the project history are first-class documents rather than wiki pages. migration-notes.md is the file to read before touching v2 code, given the config API change described earlier.
The tooling tells a parallel story. There is a .storybook directory with a storybook script bound to port 6006 and separate build:storybook and storybook:test scripts, .eslintrc, a .githooks directory, .coderabbit.yaml, vite.config.ts and tsconfig.json. The presence of .agents/, .claude/, AGENTS.md and CLAUDE.md means the project carries agent instruction files in the repository, and .coderabbit.yaml means automated review is configured. This is a repository where the contributor experience has been engineered rather than improvised.
There is a downstream relationship worth naming, because it frames the licence and the support expectations. cosmos.gl powers Cosmograph, a separate toolset for exploring complex networks and AI embeddings, whose site is cosmograph.app. The library's homepage is cosmos.gl, and its documentation and storybook are served from cosmosgl.github.io/graph. So the open-source engine is the substrate for a commercial-facing product, which means the MIT licence is doing real work in the relationship and the boundary between the engine and the product is the thing to understand before depending on it.
What the library does not do is draw axes, legends, labels or anything else a chart needs. The data contract is two Float32Arrays, one of interleaved x and y coordinates and one of interleaved source and target indices, plus config. Rendering the same data in a non-WebGL context, or adding the analytical layer on top, is your code's job, and cosmos_awesome.md at the root appears to be a list of related projects for exactly that purpose.
Editorial conclusion
Adopt cosmos.gl when you are rendering a graph large enough that a CPU layout is the bottleneck, and when you control the frontend build so an ESM-only package and a Node 22 floor are not problems. Do not adopt it to replace a small graph visualisation, where the WebGL 2 requirement and the absence of a canvas fallback buy you nothing, and do not adopt it expecting d3-force semantics, because d3 appears in the dependency tree only as type definitions and a colour scale. Verify five things. Call setConfigPartial() rather than setConfig() everywhere, because setConfig resets every unlisted value to its default. Confirm your target environment actually provides WebGL 2, since v3 moved rendering to luma.gl on WebGL 2 with no fallback described. Check that the version you pin matches the tag, because package.json reads 3.4.1 while the newest release tag is v3.4.0. Read migration-notes.md if you are coming from v2, because config-driven highlighting removed the imperative selection methods. And test the shared-Device constructor signature if you plan several graphs on one page. The deciding fact is that this is a rendering engine rather than a visualisation library, so the ergonomics you get are the ergonomics of a GPU pipeline and the data contract is two typed arrays.
Frequently asked questions
How do I install and start cosmos.gl?
Install it with npm install @cosmos.gl/graph, then create a Graph against a div, pass a config object, and push data in with setPointPositions and setLinks using Float32Arrays, calling graph.render() to draw. Positions are interleaved x and y coordinates and links are interleaved source and target indices. The package requires Node 22 or newer and npm 10 or newer to build.
What is the difference between setConfig and setConfigPartial?
setConfig resets all values to their defaults before applying what you pass, so any property you omit is lost. setConfigPartial updates individual properties without resetting the rest, and it is the method to use for any config change after construction. Note that v3.0 moved highlighting from imperative methods to config properties such as highlightedPointIndices and outlinedPointIndices, which is exactly the case where this distinction bites.
Does cosmos.gl need WebGL 2?
Yes. Version 3.0 ported rendering from regl to luma.gl on WebGL 2, and no WebGL 1 or canvas fallback is described. That rules out environments without WebGL 2, including most headless rendering setups, so anything in that category needs a software GL implementation before this library is an option.
How do I render a graph with fixed, precomputed positions?
Rendering and simulation are separate in v3. The simulation starts automatically by default and you control it with start(), stop(), pause() and unpause(), while enableSimulation is a config property changeable at runtime. Setting it false, or keeping the graph inert with simulationFriction and disabling simulationGravity as the quick start does, lets you draw positions you supply without any physics running.
Does cosmos.gl depend on d3-force?
No. The d3 packages in the dependency tree are type definitions such as @types/d3-scale and @types/d3-transition, plus d3-scale-chromatic for colour scales in examples. The force layout is a custom implementation running in WebGL 2 fragment and vertex shaders, so there is no d3-force to swap in or configure.
Can I use several cosmos.gl graphs on one page?
Yes, and they can share a GPU device. Rendering in v3 goes through luma.gl, and the constructor accepts a third argument, new Graph(div, config, devicePromise), which lets a custom Device be shared across multiple graphs instead of creating one WebGL context per graph. The README documents the signature but not the device lifecycle, so ownership and teardown behaviour need checking in the source.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cosmosgl-graph)