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

adaptive-heatmap-ranges

v1.0.1

Published

Generate adaptive heatmap color ranges from numeric values with ranks, labels, and customizable colors. Detects natural gaps in data to prevent misleading color scaling.

Readme

adaptive-heatmap-ranges

Generate adaptive heatmap color ranges from numeric values with intelligent gap detection. Works with any chart library — ApexCharts, Chart.js, D3, or plain JavaScript.

npm version License: MIT Zero Dependencies


Why This Package Exists

The Problem With Simple Min/Max Scaling

Most heatmap libraries color cells using naive min/max normalization. This means the smallest value gets green and the largest gets red, regardless of what those values actually represent.

This creates two major problems:

Problem 1: Small-only values get misleading colors

If your data is [0.03, 0.3, 0.6], naive scaling makes 0.6 appear red — but 0.6 isn't a high value in any practical context. It's still basically zero.

Problem 2: Mixed data with gaps

If your data is [0.03, 0.3, 0.6, 95, 100], there's a massive gap between 0.6 and 95. Naive scaling would paint the small values with intermediate colors, hiding the fact that they're all clustered near zero.

The Solution: Adaptive Range Generation

This package intelligently detects gaps and clusters in your data to generate meaningful color ranges. It doesn't just blindly split min-to-max — it understands the shape of your data.


Installation

npm install adaptive-heatmap-ranges
yarn add adaptive-heatmap-ranges
pnpm add adaptive-heatmap-ranges

Quick Start

import { generateHeatmapRanges, getRankForValue, toApexHeatmapRanges } from "adaptive-heatmap-ranges";

const values = [0.03, 0.3, 0.6, 95, 100];
const ranges = generateHeatmapRanges(values);

console.log(ranges);
// Find which range a value belongs to
const rank = getRankForValue(95, ranges);
// Convert for ApexCharts
const apexRanges = toApexHeatmapRanges(ranges);

Real Output Examples

Every example below shows the actual output from the package. What you see is what you get.

1. Small Values: [0.03, 0.3, 0.6, 0.7, 1]

When all values are small, the package rounds up to a nice ceiling (1) and distributes ranges evenly across that space. No false "hot" colors.

generateHeatmapRanges([0.03, 0.3, 0.6, 0.7, 1])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 0.2 | 🟢 #2ECC71 | | 2 | Low | 0.2001 | 0.4 | 🟢 #A3E635 | | 3 | Medium | 0.4001 | 0.6 | 🟡 #FACC15 | | 4 | High | 0.6001 | 0.8 | 🟠 #FB923C | | 5 | Very High | 0.8001 | 1 | 🔴 #EF4444 |

Why it works: All gaps are roughly equal (~0.27–0.3), so no gap is "dominant." The adaptive algorithm falls back to nice-ceiling equal splits, giving a proper distribution across the 0–1 range.


2. Huge Gap: [0.03, 0.3, 0.6, 95, 100]

When there's a massive gap between clusters, the package detects it and isolates the low cluster into rank 1.

generateHeatmapRanges([0.03, 0.3, 0.6, 95, 100])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 0.6 | 🟢 #2ECC71 | | 2 | Low | 0.6001 | 25.45 | 🟢 #A3E635 | | 3 | Medium | 25.4501 | 50.3 | 🟡 #FACC15 | | 4 | High | 50.3001 | 75.15 | 🟠 #FB923C | | 5 | Very High | 75.1501 | 100 | 🔴 #EF4444 |

Why it works: The gap between 0.6 and 95 is 94.4, which is vastly larger than other gaps (0.27, 0.3, 5). The algorithm detects this dominant gap and places all values ≤ 0.6 in the green "Very Low" range. Values 95 and 100 correctly land in the red "Very High" range.


3. Normal Spread: [10, 20, 30, 40, 50]

Evenly distributed values get evenly distributed ranges.

generateHeatmapRanges([10, 20, 30, 40, 50])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 10 | 🟢 #2ECC71 | | 2 | Low | 10.0001 | 20 | 🟢 #A3E635 | | 3 | Medium | 20.0001 | 30 | 🟡 #FACC15 | | 4 | High | 30.0001 | 40 | 🟠 #FB923C | | 5 | Very High | 40.0001 | 50 | 🔴 #EF4444 |

Why it works: All gaps between values are equal (10 each). No dominant gap exists, so ranges spread from 0 to 50 (nice ceiling = 50) with even intervals.


4. Large Values: [500, 750, 900, 950, 1000]

Works the same way at any scale.

generateHeatmapRanges([500, 750, 900, 950, 1000])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 200 | 🟢 #2ECC71 | | 2 | Low | 200.0001 | 400 | 🟢 #A3E635 | | 3 | Medium | 400.0001 | 600 | 🟡 #FACC15 | | 4 | High | 600.0001 | 800 | 🟠 #FB923C | | 5 | Very High | 800.0001 | 1000 | 🔴 #EF4444 |


5. Mixed With Gap: [1, 2, 3, 4, 5, 80, 90, 100]

Two clusters with a clear gap between them.

generateHeatmapRanges([1, 2, 3, 4, 5, 80, 90, 100])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 5 | 🟢 #2ECC71 | | 2 | Low | 5.0001 | 28.75 | 🟢 #A3E635 | | 3 | Medium | 28.7501 | 52.5 | 🟡 #FACC15 | | 4 | High | 52.5001 | 76.25 | 🟠 #FB923C | | 5 | Very High | 76.2501 | 100 | 🔴 #EF4444 |

Why it works: The gap between 5 and 80 (= 75) is dominant over other gaps (1, 10, 10). Values 1–5 land in "Very Low" (green), while 80–100 land in "Very High" (red).


6. Very Small Decimals: [0.001, 0.005, 0.01, 0.02, 0.05]

Handles microscopic values correctly by scaling to the nice ceiling of 0.05.

generateHeatmapRanges([0.001, 0.005, 0.01, 0.02, 0.05])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 0.02 | 🟢 #2ECC71 | | 2 | Low | 0.0201 | 0.0275 | 🟢 #A3E635 | | 3 | Medium | 0.0276 | 0.035 | 🟡 #FACC15 | | 4 | High | 0.0351 | 0.0425 | 🟠 #FB923C | | 5 | Very High | 0.0426 | 0.05 | 🔴 #EF4444 |


7. Negative to Positive: [-50, -20, 0, 30, 80]

Works correctly with negative values — no forced zero baseline when values go below zero.

generateHeatmapRanges([-50, -20, 0, 30, 80])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | -50 | -20 | 🟢 #2ECC71 | | 2 | Low | -19.9999 | 10 | 🟢 #A3E635 | | 3 | Medium | 10.0001 | 40 | 🟡 #FACC15 | | 4 | High | 40.0001 | 70 | 🟠 #FB923C | | 5 | Very High | 70.0001 | 100 | 🔴 #EF4444 |


8. Single Value: [42]

Even a single value produces proper ranges using nice ceiling logic.

generateHeatmapRanges([42])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 10 | 🟢 #2ECC71 | | 2 | Low | 10.0001 | 20 | 🟢 #A3E635 | | 3 | Medium | 20.0001 | 30 | 🟡 #FACC15 | | 4 | High | 30.0001 | 40 | 🟠 #FB923C | | 5 | Very High | 40.0001 | 50 | 🔴 #EF4444 |


9. Duplicate Values: [5, 5, 5, 5]

Handles identical values safely — uses nice ceiling to create sensible ranges.

generateHeatmapRanges([5, 5, 5, 5])

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 1 | 🟢 #2ECC71 | | 2 | Low | 1.0001 | 2 | 🟢 #A3E635 | | 3 | Medium | 2.0001 | 3 | 🟡 #FACC15 | | 4 | High | 3.0001 | 4 | 🟠 #FB923C | | 5 | Very High | 4.0001 | 5 | 🔴 #EF4444 |


10. Equal Strategy: [10, 20, 30, 40, 50]

Simple equal-width intervals without gap detection.

generateHeatmapRanges([10, 20, 30, 40, 50], { strategy: "equal" })

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 10 | 🟢 #2ECC71 | | 2 | Low | 10.0001 | 20 | 🟢 #A3E635 | | 3 | Medium | 20.0001 | 30 | 🟡 #FACC15 | | 4 | High | 30.0001 | 40 | 🟠 #FB923C | | 5 | Very High | 40.0001 | 50 | 🔴 #EF4444 |


11. Quantile Strategy: [1, 1, 2, 2, 3, 50, 100]

Ranges based on data density — more data points in a region = tighter ranges there.

generateHeatmapRanges([1, 1, 2, 2, 3, 50, 100], { strategy: "quantile" })

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 1.2 | 🟢 #2ECC71 | | 2 | Low | 1.2001 | 2 | 🟢 #A3E635 | | 3 | Medium | 2.0001 | 2.6 | 🟡 #FACC15 | | 4 | High | 2.6001 | 40.6 | 🟠 #FB923C | | 5 | Very High | 40.6001 | 100 | 🔴 #EF4444 |

Why it works: Most data points (1, 1, 2, 2, 3) cluster at low values. Quantile strategy assigns more ranges to that dense region, while the sparse high values (50, 100) share the upper ranges.


12. Reverse Mode: [10, 20, 30, 40, 50]

When high values are good (e.g., adoption scores, satisfaction ratings), reverse makes low = red and high = green.

generateHeatmapRanges([10, 20, 30, 40, 50], {
  reverse: true,
  labels: ["Poor", "Below Average", "Average", "Good", "Excellent"]
})

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Excellent | 0 | 10 | 🔴 #EF4444 | | 2 | Good | 10.0001 | 20 | 🟠 #FB923C | | 3 | Average | 20.0001 | 30 | 🟡 #FACC15 | | 4 | Below Average | 30.0001 | 40 | 🟢 #A3E635 | | 5 | Poor | 40.0001 | 50 | 🟢 #2ECC71 |


13. Custom Colors & Labels: [10, 20, 30, 40, 50]

generateHeatmapRanges([10, 20, 30, 40, 50], {
  colors: ["#00A65A", "#8CC152", "#F6BB42", "#E9573F", "#DA4453"],
  labels: ["Safe", "Low", "Moderate", "Risk", "Critical"]
})

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Safe | 0 | 10 | #00A65A | | 2 | Low | 10.0001 | 20 | #8CC152 | | 3 | Moderate | 20.0001 | 30 | #F6BB42 | | 4 | Risk | 30.0001 | 40 | #E9573F | | 5 | Critical | 40.0001 | 50 | #DA4453 |


14. Custom Steps (3 ranges): [10, 20, 30, 40, 50]

generateHeatmapRanges([10, 20, 30, 40, 50], { steps: 3 })

| Rank | Label | From | To | Color | |------|-------|------|----|-------| | 1 | Very Low | 0 | 16.6667 | 🟢 #2ECC71 | | 2 | Low | 16.6668 | 33.3333 | 🟢 #A3E635 | | 3 | Medium | 33.3334 | 50 | 🟡 #FACC15 |


How Adaptive Gap Detection Works

The default "adaptive" strategy is smarter than simple equal splitting. Here's the full algorithm:

Input: [0.03, 0.3, 0.6, 95, 100]

Step 1 — Sort & compute gaps between consecutive unique values:
  0.03 → 0.3  = 0.27
  0.3  → 0.6  = 0.3
  0.6  → 95   = 94.4   ← LARGEST
  95   → 100  = 5

Step 2 — Check if the largest gap is significant:
  Threshold = max (100) × gapSensitivity (0.3) = 30
  Is 94.4 > 30?  → YES ✓

Step 3 — Check if the gap is dominant (not just noise):
  Average of other gaps = (0.27 + 0.3 + 5) / 3 = 1.86
  Is 94.4 > 1.86 × 3?  → YES ✓ (50x larger!)

Step 4 — Split at the gap:
  Low cluster: [0.03, 0.3, 0.6] → Rank 1 (green), range 0–0.6
  Remaining: 0.6 to nice ceiling (100) split evenly into 4 ranges

Result:
  Rank 1: 0 → 0.6        🟢 Very Low
  Rank 2: 0.6001 → 25.45  🟢 Low
  Rank 3: 25.4501 → 50.3  🟡 Medium
  Rank 4: 50.3001 → 75.15 🟠 High
  Rank 5: 75.1501 → 100   🔴 Very High

Why the dominance check matters:

For [0.03, 0.3, 0.6, 0.7, 1], the gaps are 0.27, 0.3, 0.1, 0.3. The largest gap (0.3) is only 1.4× the average — it's not a real cluster boundary, just normal noise. So the algorithm falls back to equal splitting and gives you a proper 0–1 distribution.


API Reference

generateHeatmapRanges(values, options?)

Main function. Returns dynamic color ranges from numeric values.

function generateHeatmapRanges(
  values: number[],
  options?: HeatmapRangeOptions
): HeatmapRange[]

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | steps | number | 5 | Number of color ranges to generate | | strategy | HeatmapStrategy | "adaptive" | "adaptive" | "equal" | "quantile" | "natural-breaks" | | colors | string[] | Green-to-red | Custom hex color codes | | labels | string[] | ["Very Low", ...] | Custom rank labels | | min | number | Auto | Override minimum value | | max | number | Auto | Override maximum value | | roundTo | number | 4 | Decimal places for breakpoints | | includeZeroBaseline | boolean | true | Start from 0 when all values ≥ 0 | | gapSensitivity | number | 0.3 | Gap detection threshold (fraction of max) | | nearZeroRatio | number | 0.3 | Near-zero cluster ratio | | reverse | boolean | false | Reverse color/label order | | clampOutliers | boolean | false | Clamp out-of-range values | | fallbackMax | number | 1 | Max when all inputs are 0 or identical |

Output

interface HeatmapRange {
  rank: number;     // 1-based rank
  label: string;    // Human-readable label
  from: number;     // Range start (inclusive)
  to: number;       // Range end (inclusive)
  color: string;    // Hex color code
}

getRankForValue(value, ranges)

Find which range a value belongs to.

const ranges = generateHeatmapRanges([0.03, 0.3, 0.6, 95, 100]);

getRankForValue(0.3, ranges);
// → { rank: 1, label: "Very Low", from: 0, to: 0.6, color: "#2ECC71" }

getRankForValue(95, ranges);
// → { rank: 5, label: "Very High", from: 75.1501, to: 100, color: "#EF4444" }

getRankForValue(NaN, ranges);
// → null

toApexHeatmapRanges(ranges)

Convert to ApexCharts plotOptions.heatmap.colorScale.ranges format. Maps labelname.

const apexRanges = toApexHeatmapRanges(ranges);
// [{ from: 0, to: 0.6, name: "Very Low", color: "#2ECC71" }, ...]

normalizeValues(values)

Clean an array of mixed types into sorted numbers. Filters out null, undefined, NaN, Infinity, and non-numeric values.

import { normalizeValues } from "adaptive-heatmap-ranges";

normalizeValues([0.03, null, "hello", NaN, 0.6, undefined, 95]);
// → [0.03, 0.6, 95]

getNiceCeil(value)

Round up to a human-friendly ceiling using the 1-2-5-10 convention.

import { getNiceCeil } from "adaptive-heatmap-ranges";

getNiceCeil(0.3);   // → 0.5
getNiceCeil(2.7);   // → 5
getNiceCeil(8.4);   // → 10
getNiceCeil(42);    // → 50
getNiceCeil(95);    // → 100
getNiceCeil(350);   // → 500

Strategies

| Strategy | Best For | Description | |----------|----------|-------------| | "adaptive" | Most use cases | Detects gaps between clusters; prevents misleading colors | | "equal" | Uniform data | Equal-width intervals from min to max | | "quantile" | Skewed data | Equal number of data points per range | | "natural-breaks" | Clustered data | Jenks optimization — minimizes within-class variance |


ApexCharts Integration

import { generateHeatmapRanges, toApexHeatmapRanges } from "adaptive-heatmap-ranges";

const allValues = series.flatMap(row => row.data.map(point => point.y));

const ranges = generateHeatmapRanges(allValues, {
  strategy: "adaptive",
  steps: 5,
});

const options = {
  chart: { type: "heatmap" },
  plotOptions: {
    heatmap: {
      distributed: false,
      enableShades: false,
      colorScale: {
        ranges: toApexHeatmapRanges(ranges),
      },
    },
  },
};

Framework Compatibility

Zero runtime dependencies. Works everywhere.

| Environment | Support | |-------------|---------| | Node.js | ✅ ESM & CommonJS | | React / Vue / Angular / Svelte | ✅ | | Plain Browser JS | ✅ ESM import | | ApexCharts | ✅ via toApexHeatmapRanges() | | Chart.js / D3 / any chart lib | ✅ use ranges directly |


TypeScript Support

Full type declarations included:

import type {
  HeatmapRange,
  HeatmapRangeOptions,
  HeatmapStrategy,
  ApexHeatmapRange,
} from "adaptive-heatmap-ranges";

License

MIT