Some visualizations animate, pulse, and flash. If you're sensitive to motion or flashing, turn on Reduce motion in your system settings.

poopdeck.gl
deck.gl Layers

FlowLinesLayer

The FlowLinesLayer is a tapered half-arrow primitive — a port of flowmap.gl's FlowLinesLayer adapted to STT's binary-tile pipeline. One instance is one origin→destination flow: a straight shaft that tapers from the origin into a triangular arrowhead at the destination, with width proportional to the flow magnitude.

It is the geometry that FlowmapLayer renders instead of arcs. Use it directly if you have your own source/target data and want the arrow look.

How it draws#

Each instance is a fixed 9-vertex template mesh (3 triangles) extruded in the vertex shader, entirely in screen pixels (like deck.gl's own LineLayer):

  • the vertex is placed at mix(source, target) on the centerline, then offset perpendicular by template.perp · width (shaft / arrowhead half-width) and along-travel by template.travel · width (the arrowhead pull-back);
  • a constant gap · width perpendicular offset pushes the whole arrow to one side of the centerline, so the A→B and B→A flows of a pair sit side-by-side (the perpendicular direction flips with the flow direction);
  • per-instance getEndpointOffsets ([sourceInset, targetInset], pixels) inset the start/end along the line so the arrow begins/ends at the node-circle edge, not its center;
  • the along-travel and endpoint offsets are clamped to a fraction of the flow's pixel length so short flows don't self-overlap or overshoot.

Color is a layer-uniform mix(sourceColor → targetColor) along the arrow (origin tail → arrowhead).

Unlike flowmap.gl (which feeds a normalized instanceThickness∈[0,0.5] × thicknessUnit), this layer takes width directly in pixels per instance via getWidth — so a host that already computes a pixel width (e.g. FlowmapLayer's widthScale·√flow) just feeds it through.

Installation#

import { FlowLinesLayer } from '@poopdeck.gl/layers';

Usage#

const layer = new FlowLinesLayer({
id: 'flows',
data: flows,
getSourcePosition: (d) => d.from, // [lng, lat]
getTargetPosition: (d) => d.to,
getWidth: (d) => Math.sqrt(d.count), // pixels
getEndpointOffsets: (d) => [d.fromRadius, d.toRadius], // pixels
sourceColor: [56, 196, 232, 235],
targetColor: [255, 142, 64, 245],
gap: 0.5,
widthMinPixels: 1,
widthMaxPixels: 12,
});

Binary input (one instanced buffer per attribute) is also supported via deck.gl's data: { length, attributes } form, keyed by accessor name (getSourcePosition, getTargetPosition, getWidth, getEndpointOffsets) — this is how FlowmapLayer feeds it zero-copy from a tile.

Properties#

PropTypeDefaultDescription
getSourcePositionAccessor<Position>d.sourcePositionOrigin position.
getTargetPositionAccessor<Position>d.targetPositionDestination position.
getWidthAccessor<number>1Per-flow width in pixels (already scaled, not normalized).
getEndpointOffsetsAccessor<[number, number]>[0, 0][sourceInset, targetInset] in pixels — pulls the ends in so the arrow meets the node-circle edge.
sourceColorColor[0,150,255,255]Origin / tail color.
targetColorColor[255,127,14,255]Destination / arrowhead color.
gapnumber0.5Perpendicular separation of the two directions, in units of the arrow width.
widthUnits'pixels' | 'meters' | 'common''pixels'Units of getWidth, matching deck's LineLayer.widthUnits. 'pixels' keeps a constant on-screen thickness; 'meters'/'common' scale with the map.
widthScalenumber1Multiplier applied to getWidth before the pixel clamp. Zero-width (inactive) flows stay invisible whatever the scale.
widthMinPixelsnumber0Clamp width to at least this many pixels — active flows only.
widthMaxPixelsnumberNumber.MAX_SAFE_INTEGERClamp width to at most this many pixels.

A width of exactly 0 stays 0. widthMinPixels deliberately does not raise it, which is what makes FlowmapLayer's "below minFlow → invisible" animation work: inactive arrows genuinely render nothing rather than being clamped up to a hairline.

Extensions are not supported#

This is a fully custom-Model layer: it calls luma's picking module functions directly (picking_setPickingAttribute / picking_setPickingColor in the VS, picking_filterHighlightColor / picking_filterPickingColor in the FS) rather than going through deck's globally-registered DECKGL_FILTER_* shader hooks. Those hooks live on a process-wide luma ShaderAssembler singleton, which a bundler can duplicate for a separately-bundled package — leaving the hooks undefined and the shader failing to compile. Calling the module functions directly makes the layer self-contained and bundler-agnostic.

The trade-off: deck extensions that inject into DECKGL_FILTER_* (DataFilterExtension, TimeFilterExtension, CategoryColorExtension) have no effect here — their injections are silently dropped. The composites that own this primitive therefore strip a forwarded extensions list, with a one-time warning, instead of passing along something inert.

See also#

  • FlowmapLayer — animated OD flowmap that renders this primitive from vertexValueMatrix tiles.
  • AnimatedArcLayer — raised-arc OD rendering (the non-arrow alternative).

Source#

packages/layers/src/layers/internal/flow-lines-layer.ts