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

@andyball/breaks

v0.1.0

Published

Opinionated CLI and library for choosing colour scales, class breaks and palettes for data visualisation

Readme

breaks

An opinionated CLI and TypeScript library for choosing colour scales for data visualisation.

D3 already builds colour scales very well. breaks answers the question before that one:

Given this data, what colour scale should I use, where should the breaks be, and why?

breaks unemployment.csv --column rate --compare
Analysing 36 values from "rate"...

range         1.8 → 21.4
median        5.9
mean          7.34
std dev       4.54
skew          1.44
outliers      3
zero          no
negative      no
distinct      36

Recommended scale
────────────────────────────────────────────────────

type          sequential
method        pretty
classes       5

breaks        5
              10
              15
              20

palette       Blues
direction     light (low) → dark blue (high)
colours       #eff3ff #bdd7e7 #6baed6 #3182bd #08519c
confidence    69%

Reason:
• Values are numeric and ordered.
• There is no obvious meaningful midpoint.
• The distribution is strongly right-skewed (skew 1.44).
• Evenly spaced round numbers classify this data as well as any alternative.
• 3 outlier(s) were kept in the classification rather than trimmed.
• ckmeans scored almost as well (80 vs 81); see --compare.

Warnings:
  [warn] 3 outlier(s) detected (16.1, 18.4, 21.4). An outlier class may be
         appropriate, but check they are not data errors.

METHOD    BREAKS        COUNTS       SCORE
───────────────────────────────────────────
pretty    5 10 15 20    12/17/4/2/1     81 *
ckmeans   5 8 13 18     12/13/7/2/2     80
quantile  4 5.5 6.5 10  7/8/6/8/7       77
equal     6 10 14 18    18/11/3/2/2     75

Contents

Why

A statistically optimal set of breaks is not necessarily a good legend.

3.73          4
7.91    vs    8
12.48         12
19.62         20

The right-hand column is much easier to communicate, and on most datasets it classifies the data just as well. breaks treats legend readability as a first-class optimisation goal — but it verifies every rounding by reclassifying the data, and rejects any that moves more than 10% of observations into a different class.

It also explains itself, and shows its working with --compare, so you can disagree with it.

Requirements

| | | |---|---| | Node | >= 22 | | Install | ./install.sh (symlinks into ~/bin) |

Installation

npm install
./install.sh

This builds to dist/ and symlinks ~/bin/breaks at this directory, so the command always runs the working copy. npm link also works.

As a library:

npm install @andyball/breaks

Usage

# CSV
breaks data.csv --column unemployment

# JSON, including nested paths
breaks data.json --column properties.rate

# GeoJSON features are unwrapped automatically
breaks boundaries.geojson --column properties.rate

# Newline-separated values on stdin
cat values.txt | breaks

# Show every candidate and its score
breaks data.csv -c value --compare

# Explain every score component
breaks data.csv -c value --compare --verbose

Semantic hints, when the data alone cannot know:

breaks data.csv --column swing --centre 0
breaks data.csv --column rate --type sequential --classes 6
breaks data.csv --column change --centre 0 --symmetric

Options

-c, --column <name>          column to analyse; dotted paths work for JSON
    --compare                show every candidate classification and its score
-f, --format <format>        json | d3 | maplibre | css
-t, --type <type>            categorical | sequential | diverging | binary
    --centre <value>         semantic midpoint; implies a diverging scale
-n, --classes <n>            number of classes
-m, --method <method>        equal | quantile | pretty | ckmeans | logarithmic | symlog | centred
-p, --palette <name>         palette name
    --reverse                reverse the palette
    --no-nice                do not round breaks for legend readability
    --symmetric              mirror diverging breaks around the centre
    --max-distortion <f>     max share of observations a rounding may move (default 0.1)
    --reference <values>     comma-separated values a break should land on
    --property <name>        feature property name for the MapLibre expression
    --strict                 fail when any value cannot be parsed
-v, --verbose                show the full score breakdown with --compare

Every recommendation is overridable: scale type, class count, method, midpoint, palette and rounding behaviour.

Output formats

Human output is suppressed entirely when --format is set, so the result is safe to pipe.

MapLibre — the primary exporter for mapping work:

breaks data.csv -c rate --format maplibre
[
  "step",
  ["get","rate"],
  "#eff3ff",
  5, "#bdd7e7",
  10, "#6baed6",
  15, "#3182bd",
  20, "#08519c"
]

Categorical data produces a match expression with an #eee fallback.

D3:

d3.scaleThreshold()
  .domain([5, 10, 15, 20])
  .range(['#eff3ff', '#bdd7e7', '#6baed6', '#3182bd', '#08519c'])
  .unknown('#eee')

JSON — the full serialisable recommendation, with the candidate list summarised. CSS — custom properties plus a hard-stop legend gradient.

Library API

import { analyse, recommend, createColourScale } from '@andyball/breaks';

const recommendation = recommend(values, { purpose: 'choropleth', classes: 5 });
// => { scaleType, classification, breaks, rawBreaks, palette, reasons, warnings, ... }

const scale = createColourScale({
  type: 'threshold',
  domain: [4, 8, 12, 20],
  colours
});

createColourScale does no statistics — it takes a domain and builds a scale. Deciding what the domain should be is recommend()'s job. Keeping the two apart is what makes both reusable.

Supported scale types: linear, threshold, quantile, quantize, ordinal, diverging. Unknown, empty and unparseable values return #eee.

Every stage of the pipeline is also exported individually — analyse, inferScaleType, pretty, ckmeans, niceBreaks, scoreCandidate, suggestPalette — along with all types.

How the recommendation works

clean → analyse → inferScaleType → generateCandidates → niceBreaks
      → scoreCandidates → suggestPalette → recommend → exporters
  1. Clean. Currency symbols, thousands separators, percentages and accounting negatives are parsed. Null markers are counted as missing. Anything else is reported as invalid — never coerced to zero.
  2. Analyse. Everything from one sorted array: extent, mean, median, quartiles, IQR, standard deviation, skewness, unique count, zero crossing and Tukey outliers. Degenerate cases (all values identical, tiny samples) are flagged rather than allowed through.
  3. Infer the scale type. Categorical, sequential, diverging or binary. An explicit --type or --centre always wins.
  4. Generate candidates. Equal interval, quantile, pretty and ckmeans, plus centred for diverging data, logarithmic where the range spans two orders of magnitude and no value is zero or negative, and symlog wherever there is a long tail — including across zero, where a plain log scale is illegal.
  5. Nice the breaks. Snap to the 1 / 2 / 2.5 / 5 × 10ⁿ ladder, then verify by reclassifying. Reject anything above the distortion budget. (symlog opts out: it has already snapped each break individually, and rounding to a shared step would flatten the growing intervals.)
  6. Score. Statistical fit (goodness of variance fit), class balance, legend readability and semantic fit, less penalties for empty classes, near-empty classes and distortion. The near-empty penalty is proportional: a class holding 1.9% of observations is a legitimate small band, one holding 0.02% is a legend entry nobody can find, and they should not cost the same. Confidence is capped at the winner's own score — there is no point being sure about a classification that scores poorly.
  7. Choose a palette. ColorBrewer and Tableau schemes via d3-scale-chromatic. Direction is always stated in words, and reversing a palette rewrites that description — so a diverging scale cannot silently claim red means negative when it now means positive.

Class breaks follow the threshold convention throughout: n classes are described by n−1 breaks, matching d3.scaleThreshold and the MapLibre step expression. A value equal to a break falls into the upper class.

Long-tailed data

The hard case for a choropleth is a column where most areas barely move and a handful change enormously — population density, income, case counts. Equal intervals put 99% of features in one class; a log scale is illegal the moment a value is zero or negative.

symlog is the answer: round numbers at increasing intervals, fine near the middle and coarse in the tail, with a break on zero.

METHOD    BREAKS                   COUNTS                  SCORE
symlog    -20 -0.5 0 5 100 1000    40/150/50/200/30/33/23     82 *
quantile  -1.5 -0.75 0 1 2         90/80/70/100/100/86        59
pretty    0 5000 10000 15000       240/280/2/3/1              53
equal     ...                      99.7% in one class          0

Each break is snapped to the round-number ladder individually rather than to a shared step, which is what lets the intervals grow.

Warnings

breaks is meant to be an adviser, not a generator. It flags:

  • a dominant lowest class
  • a maximum far above the median
  • quantile classification separating identical values
  • too many categories for a qualitative palette
  • a diverging scale requested on one-sided data
  • empty classes
  • outliers
  • a class indistinguishable from the background
  • adjacent classes too close to tell apart
  • values that could not be parsed

Development

npm run build      # tsc -> dist/
npm run dev        # tsx src/cli/index.ts
npm test           # vitest run
npm run test:watch

Tests live beside the code as *.test.ts; the fixtures in test/fixtures/ cover the classification scenarios — uniform, right-skewed, diverging, long tail across zero, outlier, categorical, binary and all-identical.

Licence

MIT.