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 🙏

© 2024 – Pkg Stats / Ryan Hefner

avoid-overlap

v0.2.1

Published

Functions to help avoid text annotation/label overlaps

Downloads

5

Readme

avoid-overlap

Utilities for chart-makers to avoid text overlaps in their graphics. The utilities were build with D3.js and React in mind but likely work with other frameworks.

Two techniques to avoid overlaps: Nudge and Choices

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: nudge and choices.

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 |

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 |

Installation

npm install --save avoid-overlap

Usage

This library provides the class AvoidOverlap, which provides the function .run(). Each page should use just one AvoidOverlap instance.

import { AvoidOverlap } from 'avoid-overlap';

// Create an avoid-overlap instance
const avoidOverlap = new AvoidOverlap();

// Place your labels here

// Then, run the avoid overlapper
avoidOverlap.run(/* args here, see table below */);

AvoidOverlap.run(parent, labelGroups, options)

Perform the label avoidance.

Call this after you have positioned all of your labels.

| Param | Type | Default | Description | | ----------------------------- | ----------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | parent | Element | | The parent element, that contains all labels. This can be a div, an svg, etc. | | labelGroups | object[] | | An array of label groups that define how to resolve overlaps | | labelGroups[].technique | string | | The overlap avoidance technique to use ("nudge" or "choices") | | labelGroups[].nodes | Element[] | | An array of elements to avoid overlaps | | labelGroups[].margin | object | { top: 0, right: 0, bottom: 0, left: 0 } | How much extra spacing to consider for collisions with these nodes | | labelGroups[].priority | number | | What priority to give this label group in case of collision. Generally nodes with lower priority values will not be moved if they collide with nodes with higher priority values. Note that 1 is a lower priority value than 10. | | labelGroups[].render | function(node, dx, dy) | | Function that applies the given nudged position (dx, dy) to the node. Required for technique nudge. | | labelGroups[].nudgeStrategy | string | "shortest" | Strategy to choose which direction to nudge toward. "shortest" will nudge in the direction requiring the least movement, while "ordered" will use the first direction that works as specified in labelGroups[].nudgeDirections. Optional for technique nudge. | | labelGroups[].nudgeDirections | string[] | ["down", "right", "up", "left"] | Which directions to consider nudging. Optional for technique nudge. | | labelGroups[].choices | function(node)[] | | An array of functions that apply a new potential positioning for the node. Each choice will be tried in order until a solution is found. Required for technique choices. | | options | object | | Global options | | [options.includeParent] | boolean | false | Whether to consider the parent as part of the bounds | | [options.parentMargin] | object | { top: 0, right: 0, bottom: 0, left: 0 } | How much extra spacing to consider for collisions with the parent | | [options.maxAttempts] | number | 3 | How many iterations to try finding collisions before giving up | | [options.debug] | boolean | false | Whether to enable debug mode, which logs all nodes in the system and calls [options.debugFunc] to visually aid in debugging | | [options.debugFunc] | function(tree, parentBounds, uid) | defaultDebugFunc | The default debug function, which draws boxes for each label for each position in its history saved as the overlap avoider ran. tree is an RBush object containing all nodes in the system. parentBounds contains the bounding box of the parent. uid is a unique identifier for this instance of AvoidOverlap. |

Example using technique: nudge

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

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

const avoidOverlap = new AvoidOverlap();

avoidOverlap.run(
  parent,
  [
    {
      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})`);
      },
    },
  ],
  {
    includeParent: true,
  }
);

Example using technique: choices

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

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

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

const avoidOverlap = new AvoidOverlap();
avoidOverlap.run(
  parent,
  [
    {
      technique: 'choices',
      nodes: headers.nodes(),
      choices: [arrowTop, arrowBottom],
      priority: 1,
    },
  ],
  {
    includeParent: true,
  }
);

Debugging

Debugging needs to be improved. But for now the debug option may help. Setting that to true will do two things.

First it will draw colorful boxes around the labels that the system is avoiding overlaps among. Each label will have a box drawn for each position it tried. It’s not great, but it’s something.

Second it will console.log() a representation of all nodes in the system. You can copy that output into the exportedArgs control on this Storybook page to see it render there, or into the stories in test/real-world-exports.stories.ts if you are running this repo locally.

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.