npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

avoid-overlap

v0.3.1

Published

Functions to help avoid text annotation/label overlaps

Readme

avoid-overlap

Avoid text overlaps in your charts and data visualizations (automatic label placement). Uses simulated annealing to quickly find an acceptable solution.

The utilities were build with D3.js and React in mind but likely work with other frameworks.

Two techniques to avoid overlaps: Choices and Nudge

Labeling charts, maps and other graphics is more art than science, but there are some general rules we can follow to achieve good results programmatically. This library provides two label-avoidance techniques: choices and nudge.

Choices

The choices technique resolves overlaps by trying a series of positions provided by the user until it finds a combination that works. This technique works well if some number of different positions might work, for example if you are using a leader line or arrow.

In the following example, the labels were passed to avoid-overlap with a list of functions that could draw the label and an arrow in different positions: to the top left, top middle, top right, bottom left, bottom middle and bottom right. The library tries these positions until it finds a combination that works.

| Before | After | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Chart with labels overlapping | Same chart with the overlapping labels rendered using one of the provided choices so they no longer collide |

Nudge

The nudge technique resolves overlaps by simply nudging labels away from each other until they no longer collide. You can limit the nudging to specific directions and distances. This technique works well if the thing you’re labeling is an area rather than a specific point.

The following images, from a Washington Post graphic, demonstrate the nudge technique. The labels were passed to avoid-overlap, specifying that nudging the labels either down or to the right would work.

| Before | After | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | Chart with labels overlapping | Same chart with the overlapping labels nudged so they no longer collide |

Installation

npm install --save avoid-overlap

Usage

import { avoidOverlap } from 'avoid-overlap';

// Place your labels here

// Then, run avoid-overlap
avoidOverlap(/* args here, see table below */);

Example using technique: choices

import { avoidOverlap } from 'avoid-overlap';
import { selectAll } from 'd3-selection';

const arrows = selectAll('.label-arrow');

const arrowTop = (node) => {
  /* Draw the arrow at the top */
};
const arrowBottom = (node) => {
  /* Draw the arrow at the bottom */
};

avoidOverlap([
  {
    technique: 'choices',
    nodes: arrows.nodes(),
    choices: [arrowTop, arrowBottom],
    priority: 1,
  },
]);

Example using technique: nudge

import { avoidOverlap } from 'avoid-overlap';
import { select, selectAll } from 'd3-selection';

const headers = selectAll('.label-header');
const subheads = selectAll('.label-subhead');

avoidOverlap([
  {
    technique: 'nudge',
    nodes: headers.nodes(),
    render: () => {},
    priority: 1,
  },
  {
    technique: 'nudge',
    nodes: subheads.nodes(),
    priority: 2,
    render: (node, dx, dy) => {
      // Apply the nudge to the node
      const selected = select(node);
      const [x, y] = selected
        .attr('transform')
        .match(/([0-9\-\.]+)/g)
        .map((d) => +d);

      select(node).attr('transform', `translate(${x + dx}, ${y + dy})`);
    },
  },
]);

avoidOverlap(labelGroups, options)

Perform the label avoidance. Call this after you have positioned all of your labels. The parent element is inferred automatically as the deepest common ancestor of all label nodes.

labelGroups — common params

| Param | Type | Description | | - | - | - | | technique | "nudge" | "choices" | "fixed" | The overlap avoidance technique to use. nudge shifts labels by a small offset; choices picks from a list of candidate positions; fixed treats nodes as immovable obstacles. | | nodes | Element[] | An array of elements to avoid overlaps. | | margin | number | object | Extra spacing to consider for collisions with these nodes. Accepts a number (uniform) or { top, right, bottom, left }. Default: 0 | | priority | number | Priority for this label group. Higher-priority labels are kept visible when a conflict cannot be resolved. Uses quadratic weighting, so differences matter more at higher values. Default: 0 | | allowRemove | boolean | Whether the algorithm is allowed to hide this label when it cannot be placed without overlapping a higher-priority label. Set to false to always show the label, even if it overlaps. Default: true | | onRemove | (el: Element) => void | Called when a node is removed from the DOM due to an unresolvable overlap. Use this to remove highlight styles for elements that no longer have labels. |

labelGroupschoices technique

| Param | Type | Description | | - | - | - | | choices | ((el: Element) => void)[] | An array of functions that each apply a candidate position to the node. Pass an empty array to treat the node as a fixed obstacle. | | choiceBonuses | number[] | Score bonus for each choice, parallel to choices. Positive values make a choice more attractive; negative values less so. When omitted, a small penalty (-0.5) is applied to non-zero choices. |

labelGroupsnudge technique

| Param | Type | Description | | - | - | - | | render | (el: Element, deltaX: number, deltaY: number) => void | Function that applies the nudged position (dx, dy) to the node. | | directions | "up" | "right" | "down" | "left" | Which directions to consider nudging. Default: ["down", "right", "up", "left"] | | maxDistance | number | Maximum nudge distance in pixels. Default: 64 |

options

| Param | Type | Description | | - | - | - | | includeParent | boolean | Whether to treat the common ancestor's edges as collision boundaries. Default: true | | parentMargin | number | object | Margin inset from the parent boundary. Negative values allow labels to touch (but not cross) the parent edge without a collision penalty. Accepts a number (uniform) or per-side object. Default: -2 | | iterations | number | Number of simulated-annealing iterations. More iterations = better results but slower. Default: 10000 | | temperature | number | Initial temperature for simulated annealing. Higher values allow the algorithm to escape local optima early on. Most users won't need to change this. Default: 100 | | coolingRate | number | Multiplicative cooling rate per iteration (between 0 and 1). Values close to 1 cool slowly; values closer to 0 cool fast. Most users won't need to change this. Default: 0.995 | | scoreExponent | number | Exponent used in the per-label score formula: (priority + 1) ^ scoreExponent. Higher values make the highest-priority labels exponentially more valuable. Default: 2 | | seed | string | number | Seed for the random number generator. The same seed produces identical placements across runs. Default: 42 | | debug | boolean | Whether to enable debug mode, which renders a panel showing label scores and lets you toggle between the original and final layouts. Default: false |

Debugging

Setting debug: true in options renders a panel at the top of the page showing each label's technique, priority, score, and chosen position. Buttons let you toggle between the original and final layouts to see what the algorithm changed.

Development

npm run dev

Development should happen on feature branches, which should be PR-ed into the main branch.

Releasing happens using the release-please GitHub action. The action handles versioning, changelogs and publishing the package to npm.