Choropleth

코로플레스

Fills each geographic unit by value using color intensity. This demo uses an abstract tile grid, not real map data, to show the principle.

Also known as: Choropleth map단계구분도
···
html
<div class="viz">
  <svg id="svg"></svg>
  <div class="scale minor" id="scale"><span>낮음</span><i></i><span>높음</span></div>
</div>
css
body{display:block}
.viz{display:flex;flex-direction:column;width:100%;height:100%}
svg{flex:1;min-height:0;display:block;width:100%;overflow:visible}
.scale{flex:none;display:flex;align-items:center;justify-content:center;gap:6px;padding:2px 4px 4px;font:600 9px/1 system-ui,sans-serif;color:var(--muted)}
.scale i{width:56px;height:7px;border-radius:4px;background:linear-gradient(90deg, color-mix(in srgb, var(--accent) 10%, var(--surface)), var(--accent))}
.hex{transition:fill .6s ease}
js
var svg = document.getElementById('svg');
var NS = 'http://www.w3.org/2000/svg';
function se(t){ return document.createElementNS(NS,t); }
function sa(e,o){ for (var k in o) e.setAttribute(k,o[k]); }
function R(a,b){ return a + Math.random()*(b-a); }

var COLS=8, ROWS=5;
var cells = [];
for (var r=0;r<ROWS;r++) for (var c=0;c<COLS;c++) cells.push({ r:r, c:c, v:R(10,90) });
var W=300, H=200;
var polys = [];

function measure(){
  var rc = svg.getBoundingClientRect();
  W = Math.max(rc.width,10); H = Math.max(rc.height,10);
  sa(svg,{ viewBox: '0 0 ' + W + ' ' + H });
}

function hexPoints(cx,cy,r){
  var pts = [];
  for (var i=0;i<6;i++){ var a = Math.PI/180*(60*i); pts.push((cx+r*Math.cos(a)).toFixed(1)+','+(cy+r*Math.sin(a)).toFixed(1)); }
  return pts.join(' ');
}

function build(){
  svg.innerHTML = '';
  polys = [];
  var padL=10, padR=10, padT=8, padB=8;
  var pw=W-padL-padR, ph=H-padT-padB;
  var hrW = pw/(1.5*COLS+0.5);
  var hrH = ph/(Math.sqrt(3)*(ROWS+0.5));
  var hr = Math.max(4, Math.min(hrW,hrH));
  var gridW = hr*(1.5*COLS+0.5);
  var gridH = Math.sqrt(3)*hr*(ROWS+0.5);
  var offX = padL+(pw-gridW)/2+hr;
  var offY = padT+(ph-gridH)/2+hr*Math.sqrt(3)/2;
  cells.forEach(function(cell){
    var cx = offX+cell.c*1.5*hr;
    var cy = offY+cell.r*Math.sqrt(3)*hr + (cell.c%2 ? Math.sqrt(3)*hr/2 : 0);
    var poly = se('polygon');
    sa(poly,{ points: hexPoints(cx,cy,hr*0.92), class:'hex' });
    svg.appendChild(poly);
    polys.push({ el:poly, cell:cell });
  });
  paint();
}

function paint(){
  polys.forEach(function(p){
    var pct = Math.max(6, Math.min(96, p.cell.v));
    p.el.setAttribute('fill', 'color-mix(in srgb, var(--accent) ' + pct + '%, var(--surface))');
  });
}

new ResizeObserver(function(){ measure(); build(); }).observe(svg);
measure();
build();
setInterval(function(){
  cells.forEach(function(cell){ cell.v = R(8,92); });
  paint();
}, 2900);

The color rule is the same as a heatmap's — one hue, only intensity varies. The only difference is that the cells are geographic regions instead of a grid. Which is exactly where the classic trap comes from: a region with more land (even with fewer people) visually reads as "more" just because it takes up more pixels. Coloring by a raw total exaggerates large regions regardless of their actual rank.

So a choropleth should, in principle, color a rate or density (a share of population, a per-capita value) — if comparing raw totals by region is really the goal, a bubble chart (a circle at each region's center, sized by value) is more honest.

How many bins you split the values into changes the map's whole impression. Whether to keep it continuous (a gradient) or bucket it into 5-7 classes depends on the goal — either way, the legend must spell out the bin boundaries.

When to use

Use it to scan a rate or density across regions with their spatial layout intact. For comparing raw totals, a bubble chart is more honest.