Choropleth Fill
The regions of a map fill with colour in a sweep across it, deeper shades for bigger values, so the pattern appears as you watch. A legend shows the scale underneath, and hovering or tapping a region shows its name and value.
How to use it
- Paste the snippet just before
</head>. - Add
data-choropleth-fillto a container holding your map, usually an inline SVG. - Give each region
data-valueand, for the tip,data-name. Regions can be SVG shapes or groups, or plain HTML boxes.
Options
Add any of these attributes to the same element.
data-value | On a region: its number. |
data-name | On a region: its name, shown in the tip. |
data-colors | Colours for the lowest and highest values. Default #ede9fe,#5b21b6. |
data-steps | How many colour classes. Default 5; 0 for a smooth scale. |
data-min / data-max | Ends of the scale. Default: the lowest and highest values. |
data-label | Title for the legend. |
data-unit | Shown after values, such as %. |
data-order | value fills from lowest to highest. Default: sweeps left to right. |
data-duration | Time for the whole sweep. Default 1600 ms. |
data-loop | Drain the colour and fill again. |
On an SVG <g> region, its first shape is filled and any text inside turns light or dark to stay readable. The numbers in the demo are made up.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Choropleth Fill: the regions of a map fill with colour one after
// another, darker for bigger values, sweeping across the map. A legend
// shows the scale, and hovering or tapping a region shows its value.
// Usage:
// <div data-choropleth-fill data-label="Rainfall (mm)">
// <svg viewBox="0 0 400 300">
// <path data-value="42" data-name="North" d="…"/>
// <path data-value="87" data-name="South" d="…"/>
// </svg>
// </div>
// Every element with data-value is a region: SVG shapes, or HTML boxes.
// On an SVG <g>, its first shape is filled and any text inside turns
// light or dark to stay readable.
// Optional: data-colors (low and high colour, default #ede9fe,#5b21b6),
// data-steps (number of colour classes, default 5; 0 for a smooth
// scale), data-min / data-max (scale ends, default the data's range),
// data-label (legend title), data-unit (after values, e.g. "%"),
// data-order ("value" fills low to high; default sweeps left to right),
// data-duration (ms for the whole sweep, default 1600), data-loop
// (drain and fill again), data-name on a region (its name in the tip).
var DEFAULTS = { colors: '#ede9fe,#5b21b6', steps: 5, duration: 1600 };
var EMPTY = '#ece8f3';
var css =
'.cpl{position:relative;line-height:1.2}' +
'.cpl svg{display:block;width:100%;height:auto;overflow:visible}' +
'.cpl [data-value]{transition:fill .55s ease,background-color .55s ease,opacity .3s}' +
'.cpl text{transition:fill .55s ease;pointer-events:none}' +
'.cpl-hot{opacity:.75}' +
'.cpl-legend{display:flex;align-items:center;gap:.6em;margin-top:.9em;font-size:.82em;color:#6b6280}' +
'.cpl-legend b{font-weight:700;color:#2a1d40;margin-right:.2em}' +
'.cpl-bar{display:flex;flex:1 1 auto;max-width:14em;height:.7em;border-radius:99px;overflow:hidden}' +
'.cpl-bar i{flex:1}' +
'.cpl-tip{position:absolute;z-index:2;transform:translate(-50%,-115%);padding:.3em .65em;border-radius:.5em;background:#1f1530;color:#fff;font-size:.82em;font-weight:700;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .2s}' +
'.cpl-tip.cpl-on{opacity:1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rgb(hex) {
var h = hex.trim().replace('#', '');
if (h.length === 3) h = h.replace(/./g, '$&$&');
var n = parseInt(h, 16);
return [n >> 16 & 255, n >> 8 & 255, n & 255];
}
function mix(a, b, t) {
return 'rgb(' + a.map(function (v, i) { return Math.round(v + (b[i] - v) * t); }).join(',') + ')';
}
function light(c) {
var m = /(\d+),(\d+),(\d+)/.exec(c);
return m && (0.299 * m[1] + 0.587 * m[2] + 0.114 * m[3]) > 150;
}
function num(v) { return Math.abs(v) >= 100 || v % 1 === 0 ? Math.round(v).toString() : v.toFixed(1); }
function setup(el) {
var d = el.dataset;
var s = (el._cpl = { regions: [] });
el.classList.add('cpl');
var cols = (d.colors || DEFAULTS.colors).split(',');
var lo = rgb(cols[0]), hi = rgb(cols[cols.length - 1]);
var steps = d.steps !== undefined ? +d.steps : DEFAULTS.steps;
var nodes = Array.from(el.querySelectorAll('[data-value]'));
var vals = nodes.map(function (n) { return parseFloat(n.getAttribute('data-value')); });
var min = d.min !== undefined ? +d.min : Math.min.apply(null, vals);
var max = d.max !== undefined ? +d.max : Math.max.apply(null, vals);
var unit = d.unit || '';
function colorFor(v) {
var t = max > min ? Math.max(0, Math.min(1, (v - min) / (max - min))) : 1;
if (steps > 1) t = Math.min(steps - 1, Math.floor(t * steps)) / (steps - 1);
return mix(lo, hi, t);
}
nodes.forEach(function (n, i) {
var svg = n instanceof SVGElement;
var target = svg && n.tagName.toLowerCase() === 'g' ? n.querySelector('path,rect,circle,ellipse,polygon') : n;
var r = {
node: n, target: target, svg: svg, value: vals[i], color: colorFor(vals[i]),
texts: svg ? Array.from(n.querySelectorAll('text')) : [],
name: n.getAttribute('data-name') || n.getAttribute('aria-label') || (n.querySelector('title') || {}).textContent || '',
};
s.regions.push(r);
paint(r, EMPTY);
if (svg && r.name) {
n.setAttribute('role', 'img');
n.setAttribute('aria-label', r.name + ': ' + num(r.value) + unit);
}
});
// Legend.
var legend = document.createElement('div');
legend.className = 'cpl-legend';
var bar = '<span class="cpl-bar" aria-hidden="true">';
var n = steps > 1 ? steps : 12;
for (var k = 0; k < n; k++) bar += '<i style="background:' + mix(lo, hi, k / (n - 1)) + '"></i>';
legend.innerHTML = (d.label ? '<b></b>' : '') + '<span>' + num(min) + unit + '</span>' + bar + '</span><span>' + num(max) + unit + '</span>';
if (d.label) legend.firstChild.textContent = d.label;
el.appendChild(legend);
tips(el, unit);
}
function paint(r, color) {
if (r.svg) r.target.style.fill = color;
else r.target.style.backgroundColor = color;
r.texts.forEach(function (t) { t.style.fill = color === EMPTY || light(color) ? '#2a1d40' : '#fff'; });
}
function tips(el, unit) {
var s = el._cpl;
if (el.closest('[aria-hidden="true"]')) return;
var tip = document.createElement('div');
tip.className = 'cpl-tip';
tip.setAttribute('aria-hidden', 'true');
el.appendChild(tip);
var hot = null;
function show(r) {
if (hot) hot.node.classList.remove('cpl-hot');
hot = r;
if (!r) { tip.classList.remove('cpl-on'); return; }
r.node.classList.add('cpl-hot');
var box = r.node.getBoundingClientRect(), me = el.getBoundingClientRect();
tip.textContent = (r.name ? r.name + ' · ' : '') + num(r.value) + unit;
tip.style.left = Math.max(40, Math.min(me.width - 40, box.left + box.width / 2 - me.left)) + 'px';
tip.style.top = (box.top - me.top) + 'px';
tip.classList.add('cpl-on');
}
s.regions.forEach(function (r) {
r.node.addEventListener('pointerenter', function () { show(r); });
r.node.addEventListener('pointerdown', function () { show(r); });
r.node.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') show(null); });
});
el.addEventListener('pointerleave', function () { show(null); });
}
function order(el) {
var s = el._cpl;
if (el.dataset.order === 'value') return s.regions.slice().sort(function (a, b) { return a.value - b.value; });
return s.regions.map(function (r) {
var b = r.node.getBoundingClientRect();
return { r: r, k: b.left + b.top * 0.35 };
}).sort(function (a, b) { return a.k - b.k; }).map(function (o) { return o.r; });
}
function fill(el) {
var s = el._cpl, list = order(el), dur = +el.dataset.duration || DEFAULTS.duration;
s.timers = [];
list.forEach(function (r, i) {
s.timers.push(setTimeout(function () { paint(r, r.color); }, i / Math.max(1, list.length - 1) * dur));
});
if (el.dataset.loop === undefined) return;
setTimeout(function () {
list.slice().reverse().forEach(function (r, i) {
setTimeout(function () { paint(r, EMPTY); }, i / Math.max(1, list.length - 1) * dur * 0.5);
});
setTimeout(function () { fill(el); }, dur * 0.5 + 900);
}, dur + 3200);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
setTimeout(function () { fill(e.target); }, 300);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-choropleth-fill]').forEach(function (el) {
if (el._cpl) return;
setup(el);
if (reduced()) el._cpl.regions.forEach(function (r) { paint(r, r.color); });
else io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-choropleth-fill data-label="Rainfall" data-unit=" mm">
<svg viewBox="0 0 400 300">
<path data-value="42" data-name="North" d="M0 0H200V150H0Z"/>
<path data-value="87" data-name="East" d="M200 0H400V150H200Z"/>
<path data-value="15" data-name="South" d="M0 150H400V300H0Z"/>
</svg>
</div>