09Picker Whose Treemap Blocks Resize to Match Each Value
Each item's value divided by the running total becomes a rectangle's area, and adjusting one item recalculates and redivides every other rectangle to match the new ratios.
Best forCategory budget or capacity allocation selection
OTHER_RATIOMIN_Vclip-path

<main class="stage">
<figure class="tm is-demo" id="tm">
<div class="tm__head">
<span class="tm__title">Item A's ratio</span>
<span class="tm__stat" aria-hidden="true"><b class="tm__n" id="tm-val">38</b>%</span>
</div>
<div class="tm__zone">
<div class="tm__map" id="map" role="img" aria-label="A 4-block treemap; adjusting value A recalculates the rectangle sizes of block A and the other three blocks (B, C, D) to match the new ratios.">
<div class="tm__block tm__block--a" id="blk-a"><b>A</b><span id="pct-a">38%</span></div>
<div class="tm__block tm__block--b" id="blk-b"><b>B</b><span id="pct-b">31%</span></div>
<div class="tm__block tm__block--c" id="blk-c"><b>C</b><span id="pct-c">19%</span></div>
<div class="tm__block tm__block--d" id="blk-d"><b>D</b><span id="pct-d">12%</span></div>
</div>
</div>
<div class="tm__track" id="track">
<span class="tm__rail" aria-hidden="true"></span>
<button class="tm__grab" id="grab" type="button" role="slider" aria-orientation="horizontal"
aria-label="Choose item A's value" aria-valuemin="5" aria-valuemax="70"
aria-valuenow="38" aria-valuetext="Item A 38%">
<svg class="tm__ic" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><rect x="32" y="32" width="192" height="192" rx="12" opacity=".2"/><path d="M208 32H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 16v56h-64V48Zm-80 0v56H48V48ZM48 120h64v88H48Zm80 88v-88h80v88Z"/></g></svg>
</button>
</div>
<p class="tm__hint">Drag on the bar or press the arrow keys and all four blocks' sizes recompute.</p>
<div class="sr">
<span aria-live="polite" id="tm-live"></span>
</div>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
// 트리맵 블록 리사이즈 피커 — A 항목 값(5~70)을 드래그·키보드로 정하면, 나머지 값(100-A)을
// 고정 비율(OTHER_RATIO)로 B·C·D 에 나눈 뒤, 네 항목의 값을 전체 합(100)으로 나눈 비율을
// 사각형 넓이(왼쪽 A 열 너비 + 오른쪽 B/C/D 행 높이)로 바꿔 매 프레임 전부 다시 계산해
// 그린다. 값은 관성 lerp 로 목표를 뒤따른다.
// CSS @keyframes 는 0개(값이 매 프레임 실수로 바뀌어 선언형으로 못 담는다, 398-01~08 과 같은 이유).
// picker-graph-treemap-other-ratio(JS, OTHER_RATIO)가 B:C:D 고정 비율을 정한다.
$duration: $dur-loop;
$easing: $ease-lerp-settle;
$color: $subject-blue;
@include stage($stage-ink);
.sr {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip-path: inset(50%); white-space: nowrap;
}
.tm {
box-sizing: border-box;
width: min(424px, 92vw);
margin: 0; padding: $sp-3;
display: flex; flex-direction: column; gap: $sp-2;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-raised;
color: $subject-ink;
}
.tm__head { display: flex; align-items: baseline; justify-content: flex-end; gap: $sp-2; padding: 0 $sp-1; }
.tm__title { font-size: 13px; line-height: 18px; font-weight: 800; letter-spacing: .02em; margin-right: auto; }
.tm__stat { font-size: 12px; line-height: 18px; font-weight: 800; font-variant-numeric: tabular-nums; color: rgba($subject-ink, .62); }
.tm__n { color: $subject-blue-deep; }
.tm__zone { padding: 0 $sp-1; }
.tm__map { position: relative; width: 100%; height: 116px; border-radius: $r-sm; overflow: hidden; }
.tm__block {
position: absolute;
box-sizing: border-box;
display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-end;
gap: $sp-1;
padding: $sp-1 $sp-2;
overflow: hidden;
border: 1px solid rgba($stage-paper, .5);
color: $stage-paper;
font-size: 11px; line-height: 14px; font-weight: 800;
}
.tm__block b { font-size: 12px; }
.tm__block span { font-variant-numeric: tabular-nums; opacity: .86; }
.tm__block--a { background: $subject-blue-deep; }
.tm__block--b { background: $subject-blue; }
.tm__block--c { background: $subject-blue-light; }
.tm__block--d { background: mix($subject-blue-light, $stage-paper, 45%); color: $subject-ink; }
.tm__track {
position: relative;
height: 24px;
margin: 0 $sp-1;
border-radius: $r-pill;
background: mix($subject-blue, $stage-paper, 14%);
}
.tm__rail {
position: absolute; left: $sp-3; right: $sp-3; top: 50%;
height: 3px; transform: translateY(-50%);
border-radius: $r-pill;
background: rgba($subject-ink, .18);
}
.tm__grab {
position: absolute; top: 50%; left: 50%;
width: 26px; height: 26px;
margin: 0; padding: 0; border: 0; border-radius: 50%;
display: grid; place-items: center;
background: $subject-blue-deep;
color: $stage-paper;
transform: translate(-50%, -50%);
cursor: ew-resize;
touch-action: none;
}
.tm__grab:focus-visible { outline: 2px solid $subject-blue-deep; outline-offset: 2px; }
.tm__ic { width: 13px; height: 13px; }
.tm__hint { margin: 0; padding: 0 $sp-1; font-size: 11px; line-height: 16px; color: rgba($subject-ink, .62); }
@media (max-width: 340px) {
.tm { padding: $sp-2; gap: $sp-1; }
.tm__hint { display: none; }
.tm__title { font-size: 12px; line-height: 16px; }
.tm__stat { font-size: 11px; line-height: 16px; }
.tm__map { height: 108px; }
.tm__grab { width: 22px; height: 22px; }
}
@media (prefers-reduced-motion: reduce) {
.tm.is-still .tm__block { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.tm {
box-sizing: border-box;
width: min(424px, 92vw);
margin: 0;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
color: #17141a;
}
.tm__head {
display: flex;
align-items: baseline;
justify-content: flex-end;
gap: 8px;
padding: 0 4px;
}
.tm__title {
font-size: 13px;
line-height: 18px;
font-weight: 800;
letter-spacing: 0.02em;
margin-right: auto;
}
.tm__stat {
font-size: 12px;
line-height: 18px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: rgba(23, 20, 26, 0.62);
}
.tm__n {
color: #0a4fe8;
}
.tm__zone {
padding: 0 4px;
}
.tm__map {
position: relative;
width: 100%;
height: 116px;
border-radius: 8px;
overflow: hidden;
}
.tm__block {
position: absolute;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
gap: 4px;
padding: 4px 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.5);
color: #ffffff;
font-size: 11px;
line-height: 14px;
font-weight: 800;
}
.tm__block b {
font-size: 12px;
}
.tm__block span {
font-variant-numeric: tabular-nums;
opacity: 0.86;
}
.tm__block--a {
background: #0a4fe8;
}
.tm__block--b {
background: #2f6df6;
}
.tm__block--c {
background: #5b8cf8;
}
.tm__block--d {
background: rgb(71.0588235294%, 79.7058823529%, 98.7647058824%);
color: #17141a;
}
.tm__track {
position: relative;
height: 24px;
margin: 0 4px;
border-radius: 999px;
background: rgb(88.5803921569%, 91.9843137255%, 99.5058823529%);
}
.tm__rail {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
height: 3px;
transform: translateY(-50%);
border-radius: 999px;
background: rgba(23, 20, 26, 0.18);
}
.tm__grab {
position: absolute;
top: 50%;
left: 50%;
width: 26px;
height: 26px;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
display: grid;
place-items: center;
background: #0a4fe8;
color: #ffffff;
transform: translate(-50%, -50%);
cursor: ew-resize;
touch-action: none;
}
.tm__grab:focus-visible {
outline: 2px solid #0a4fe8;
outline-offset: 2px;
}
.tm__ic {
width: 13px;
height: 13px;
}
.tm__hint {
margin: 0;
padding: 0 4px;
font-size: 11px;
line-height: 16px;
color: rgba(23, 20, 26, 0.62);
}
@media (max-width: 340px) {
.tm {
padding: 8px;
gap: 4px;
}
.tm__hint {
display: none;
}
.tm__title {
font-size: 12px;
line-height: 16px;
}
.tm__stat {
font-size: 11px;
line-height: 16px;
}
.tm__map {
height: 108px;
}
.tm__grab {
width: 22px;
height: 22px;
}
}
@media (prefers-reduced-motion: reduce) {
.tm.is-still .tm__block {
transition: none !important;
}
}// 실제 동작: pointerdown/move/up 과 keydown 이 Item A Value(5~70)을 바꾸면, 나머지 Value
// (100-A)을 고정 Ratio(OTHER_RATIO)로 B·C·D 에 나눈 뒤, 네 항목의 값을 전체 합(100)으로
// 나눈 비율을 사각형 넓이(왼쪽 A 열 너비 + 오른쪽 B/C/D 행 높이)로 바꿔 매 프레임 전부
// 다시 계산해 그린다. 값은 관성 lerp 로 목표를 뒤따른다.
// CSS @keyframes 없음(값이 매 프레임 실수로 바뀌어 선언형으로 못 담는다, 398-01~08 과 같은 이유).
var root = document.getElementById('tm');
var track = document.getElementById('track');
var grab = document.getElementById('grab');
var blkA = document.getElementById('blk-a');
var blkB = document.getElementById('blk-b');
var blkC = document.getElementById('blk-c');
var blkD = document.getElementById('blk-d');
var pctA = document.getElementById('pct-a');
var pctB = document.getElementById('pct-b');
var pctC = document.getElementById('pct-c');
var pctD = document.getElementById('pct-d');
var valEl = document.getElementById('tm-val');
var live = document.getElementById('tm-live');
var MIN_V = 5, MAX_V = 70;
var OTHER_RATIO = [0.5, 0.3, 0.2]; // picker-graph-treemap-other-ratio — B:C:D 고정 Ratio
var LERP_RATE = 0.2;
var AUTO_PERIOD = 2.0;
var STEP = 5;
var value = 38;
var current = 38;
var manual = false;
var autoT = 0;
var lastTs = 0;
var trackRect = null;
var rafId = 0;
function reduced() {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function paint() {
var a = current;
var rest = 100 - a;
var b = rest * OTHER_RATIO[0];
var c = rest * OTHER_RATIO[1];
var d = rest * OTHER_RATIO[2];
blkA.style.width = a + '%';
blkA.style.height = '100%';
blkA.style.left = '0%';
blkA.style.top = '0%';
var colW = 100 - a;
var bH = rest > 0.001 ? (b / rest) * 100 : 0;
var cH = rest > 0.001 ? (c / rest) * 100 : 0;
blkB.style.left = a + '%';
blkB.style.width = colW + '%';
blkB.style.top = '0%';
blkB.style.height = bH + '%';
blkC.style.left = a + '%';
blkC.style.width = colW + '%';
blkC.style.top = bH + '%';
blkC.style.height = cH + '%';
blkD.style.left = a + '%';
blkD.style.width = colW + '%';
blkD.style.top = (bH + cH) + '%';
blkD.style.height = (100 - bH - cH) + '%';
pctA.textContent = Math.round(a) + '%';
pctB.textContent = Math.round(b) + '%';
pctC.textContent = Math.round(c) + '%';
pctD.textContent = Math.round(d) + '%';
var rv = Math.round(current);
valEl.textContent = String(rv);
grab.style.left = ((value - MIN_V) / (MAX_V - MIN_V) * 100) + '%';
grab.setAttribute('aria-valuenow', String(Math.round(value)));
grab.setAttribute('aria-valuetext', 'Item A ' + Math.round(value) + '%');
}
function announce() {
live.textContent = 'Item A ' + Math.round(current) + '% — all four blocks recomputed';
}
function setValue(v, isManual) {
value = Math.max(MIN_V, Math.min(MAX_V, v));
if (isManual) { manual = true; root.classList.remove('is-demo'); }
}
function fromPointer(clientX) {
if (!trackRect) trackRect = track.getBoundingClientRect();
var ratio = (clientX - trackRect.left) / trackRect.width;
setValue(MIN_V + ratio * (MAX_V - MIN_V), true);
}
function reflectNow() { current = value; paint(); announce(); }
function onDown(e) {
trackRect = track.getBoundingClientRect();
if (grab.setPointerCapture) { try { grab.setPointerCapture(e.pointerId); } catch (err) {} }
fromPointer(e.clientX);
if (reduced()) reflectNow();
}
function onMove(e) { fromPointer(e.clientX); if (reduced()) reflectNow(); }
function onUp() { announce(); }
function onTrackDown(e) { trackRect = track.getBoundingClientRect(); fromPointer(e.clientX); if (reduced()) reflectNow(); }
function onKey(e) {
var map = { ArrowLeft: -STEP, ArrowDown: -STEP, ArrowRight: STEP, ArrowUp: STEP };
if (e.key === 'Home') { setValue(MIN_V, true); e.preventDefault(); }
else if (e.key === 'End') { setValue(MAX_V, true); e.preventDefault(); }
else if (map[e.key] !== undefined) { setValue(value + map[e.key], true); e.preventDefault(); }
else { return; }
if (reduced()) reflectNow();
}
function onResize() { trackRect = track.getBoundingClientRect(); if (reduced()) paint(); }
function autoStep(dt) {
autoT += dt;
var omega = 2 * Math.PI / AUTO_PERIOD;
var mid = (MIN_V + MAX_V) / 2;
var amp = (MAX_V - MIN_V) / 2 * 0.92;
value = mid + amp * Math.sin(autoT * omega);
}
function frame(ts) {
if (!lastTs) lastTs = ts;
var dt = Math.min(0.05, (ts - lastTs) / 1000);
lastTs = ts;
if (!manual) autoStep(dt);
current += (value - current) * LERP_RATE;
paint();
rafId = requestAnimationFrame(frame);
}
grab.addEventListener('pointerdown', onDown);
grab.addEventListener('pointermove', onMove);
grab.addEventListener('pointerup', onUp);
grab.addEventListener('keydown', onKey);
track.addEventListener('pointerdown', onTrackDown);
window.addEventListener('resize', onResize);
if (reduced()) {
root.classList.add('is-still');
current = value;
paint();
} else {
paint();
rafId = requestAnimationFrame(frame);
}// TreemapBlockResize — vanilla 판(run/398/demo/09-picker-graph-treemap-block-resize/index.html) 의
// React 포트. A 항목 값을 조정할 때마다 나머지 값(100-A)을 고정 비율로 B·C·D 에 나눈 뒤,
// 네 항목의 값을 전체 합(100)으로 나눈 비율을 사각형 넓이로 바꿔 매 프레임 전부 다시 계산해 그린다.
import { useEffect, useRef } from "react";
import styles from "./TreemapBlockResize.module.scss";
type Box<T> = { current: T };
type KeyEvt = { key: string; preventDefault(): void };
type PtrEvt = { clientX: number; pointerId: number };
const MIN_V = 5, MAX_V = 70;
const OTHER_RATIO = [0.5, 0.3, 0.2]; // picker-graph-treemap-other-ratio — B:C:D 고정 비율
const LERP_RATE = 0.2;
const AUTO_PERIOD = 2.0;
const STEP = 5;
export default function TreemapBlockResize() {
const rootRef = useRef<HTMLElement | null>(null);
const trackRef = useRef<HTMLDivElement | null>(null);
const grabRef = useRef<HTMLButtonElement | null>(null);
const blkARef = useRef<HTMLDivElement | null>(null);
const blkBRef = useRef<HTMLDivElement | null>(null);
const blkCRef = useRef<HTMLDivElement | null>(null);
const blkDRef = useRef<HTMLDivElement | null>(null);
const pctARef = useRef<HTMLElement | null>(null);
const pctBRef = useRef<HTMLElement | null>(null);
const pctCRef = useRef<HTMLElement | null>(null);
const pctDRef = useRef<HTMLElement | null>(null);
const valRef = useRef<HTMLElement | null>(null);
const liveRef = useRef<HTMLSpanElement | null>(null);
const value = useRef(38) as Box<number>;
const current = useRef(38) as Box<number>;
const manual = useRef(false) as Box<boolean>;
const autoT = useRef(0) as Box<number>;
const lastTs = useRef(0) as Box<number>;
const trackRect = useRef<DOMRect | null>(null) as Box<DOMRect | null>;
const rafId = useRef(0) as Box<number>;
function reduced(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function paint(): void {
const a = current.current;
const rest = 100 - a;
const b = rest * OTHER_RATIO[0];
const c = rest * OTHER_RATIO[1];
const d = rest * OTHER_RATIO[2];
const colW = 100 - a;
const bH = rest > 0.001 ? (b / rest) * 100 : 0;
const cH = rest > 0.001 ? (c / rest) * 100 : 0;
const blkA = blkARef.current;
if (blkA) { blkA.style.left = "0%"; blkA.style.top = "0%"; blkA.style.width = a + "%"; blkA.style.height = "100%"; }
const blkB = blkBRef.current;
if (blkB) { blkB.style.left = a + "%"; blkB.style.top = "0%"; blkB.style.width = colW + "%"; blkB.style.height = bH + "%"; }
const blkC = blkCRef.current;
if (blkC) { blkC.style.left = a + "%"; blkC.style.top = bH + "%"; blkC.style.width = colW + "%"; blkC.style.height = cH + "%"; }
const blkD = blkDRef.current;
if (blkD) { blkD.style.left = a + "%"; blkD.style.top = bH + cH + "%"; blkD.style.width = colW + "%"; blkD.style.height = 100 - bH - cH + "%"; }
if (pctARef.current) pctARef.current.textContent = Math.round(a) + "%";
if (pctBRef.current) pctBRef.current.textContent = Math.round(b) + "%";
if (pctCRef.current) pctCRef.current.textContent = Math.round(c) + "%";
if (pctDRef.current) pctDRef.current.textContent = Math.round(d) + "%";
const rv = Math.round(current.current);
if (valRef.current) valRef.current.textContent = String(rv);
const grab = grabRef.current;
if (grab) {
grab.style.left = ((value.current - MIN_V) / (MAX_V - MIN_V)) * 100 + "%";
grab.setAttribute("aria-valuenow", String(Math.round(value.current)));
grab.setAttribute("aria-valuetext", "A 항목 " + Math.round(value.current) + "%");
}
}
function announce(): void {
if (liveRef.current) liveRef.current.textContent = "A 항목 " + Math.round(current.current) + "%로 네 블록 모두 다시 계산됨";
}
function setValue(v: number, isManual: boolean): void {
value.current = Math.max(MIN_V, Math.min(MAX_V, v));
if (isManual) {
manual.current = true;
rootRef.current?.classList.remove("is-demo");
}
}
function fromPointer(clientX: number): void {
if (!trackRect.current && trackRef.current) trackRect.current = trackRef.current.getBoundingClientRect();
const rect = trackRect.current;
if (!rect) return;
const ratio = (clientX - rect.left) / rect.width;
setValue(MIN_V + ratio * (MAX_V - MIN_V), true);
}
function reflectNow(): void {
current.current = value.current;
paint();
announce();
}
function onDown(e: PtrEvt): void {
if (trackRef.current) trackRect.current = trackRef.current.getBoundingClientRect();
const grab = grabRef.current;
if (grab && typeof grab.setPointerCapture === "function") {
try { grab.setPointerCapture(e.pointerId); } catch { /* 캡처 실패는 치명적이지 않다 */ }
}
fromPointer(e.clientX);
if (reduced()) reflectNow();
}
function onMove(e: PtrEvt): void {
fromPointer(e.clientX);
if (reduced()) reflectNow();
}
function onUp(): void {
announce();
}
function onTrackDown(e: PtrEvt): void {
if (trackRef.current) trackRect.current = trackRef.current.getBoundingClientRect();
fromPointer(e.clientX);
if (reduced()) reflectNow();
}
function onKey(e: KeyEvt): void {
const map: Record<string, number> = { ArrowLeft: -STEP, ArrowDown: -STEP, ArrowRight: STEP, ArrowUp: STEP };
if (e.key === "Home") { setValue(MIN_V, true); e.preventDefault(); }
else if (e.key === "End") { setValue(MAX_V, true); e.preventDefault(); }
else if (map[e.key] !== undefined) { setValue(value.current + map[e.key], true); e.preventDefault(); }
else { return; }
if (reduced()) reflectNow();
}
function onResize(): void {
if (trackRef.current) trackRect.current = trackRef.current.getBoundingClientRect();
if (reduced()) paint();
}
function autoStep(dt: number): void {
autoT.current += dt;
const omega = (2 * Math.PI) / AUTO_PERIOD;
const mid = (MIN_V + MAX_V) / 2;
const amp = ((MAX_V - MIN_V) / 2) * 0.92;
value.current = mid + amp * Math.sin(autoT.current * omega);
}
function frame(ts: number): void {
if (!lastTs.current) lastTs.current = ts;
const dt = Math.min(0.05, (ts - lastTs.current) / 1000);
lastTs.current = ts;
if (!manual.current) autoStep(dt);
current.current += (value.current - current.current) * LERP_RATE;
paint();
rafId.current = requestAnimationFrame(frame);
}
useEffect(() => {
window.addEventListener("resize", onResize);
if (reduced()) {
rootRef.current?.classList.add("is-still");
reflectNow();
} else {
paint();
rafId.current = requestAnimationFrame(frame);
}
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(rafId.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<main className={styles.stage}>
<figure className={styles.tm + " is-demo"} ref={rootRef}>
<div className={styles.head}>
<span className={styles.title}>A 항목 비율</span>
<span className={styles.stat} aria-hidden="true"><b className={styles.n} ref={valRef}>38</b>%</span>
</div>
<div className={styles.zone}>
<div
className={styles.map}
role="img"
aria-label="4블록 트리맵, A 값을 조정할 때마다 A 블록과 나머지 세 블록(B·C·D)의 사각형 크기가 비율에 맞춰 전부 다시 계산된다"
>
<div className={styles.block + " " + styles.a} ref={blkARef}><b>A</b><span ref={pctARef}>38%</span></div>
<div className={styles.block + " " + styles.b} ref={blkBRef}><b>B</b><span ref={pctBRef}>31%</span></div>
<div className={styles.block + " " + styles.c} ref={blkCRef}><b>C</b><span ref={pctCRef}>19%</span></div>
<div className={styles.block + " " + styles.d} ref={blkDRef}><b>D</b><span ref={pctDRef}>12%</span></div>
</div>
</div>
<div className={styles.track} ref={trackRef} onPointerDown={onTrackDown}>
<span className={styles.rail} aria-hidden="true" />
<button
className={styles.grab}
ref={grabRef}
type="button"
role="slider"
aria-orientation="horizontal"
aria-label="A 항목 값 고르기"
aria-valuemin={MIN_V}
aria-valuemax={MAX_V}
aria-valuenow={38}
aria-valuetext="A 항목 38%"
onPointerDown={onDown}
onPointerMove={onMove}
onPointerUp={onUp}
onKeyDown={onKey}
>
<svg className={styles.ic} viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<rect x={32} y={32} width={192} height={192} rx={12} opacity={0.2} />
<path d="M208 32H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 16v56h-64V48Zm-80 0v56H48V48ZM48 120h64v88H48Zm80 88v-88h80v88Z" />
</g>
</svg>
</button>
</div>
<p className={styles.hint}>막대를 드래그하거나 화살표키를 누르면 네 블록의 크기가 모두 다시 계산됩니다</p>
<div className={styles.sr}>
<span aria-live="polite" ref={liveRef} />
</div>
</figure>
</main>
);
}09 Picker Whose Treemap Blocks Resize to Match Each Value — Measured area 24.647% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 Graph Animation CSS Pickers With Live Recompute
The post this demo lives in
9 Graph Animation CSS Pickers With Live RecomputeMove a bar by pixel and the graph drifts from its value. So nine pickers recompute: bar height from a bucket value, a ga See all demos in this post
If you like this effect - similar demos
Pickers03 Keyboard-arrow value toggle — each time a direction value changes, a highlighted-cell value moves across the gridDate Picker UI: 9 Popover & Calendar Patterns
Pickers04 Span bar whose two handles push each other9 time picker ui patterns, from a point to a span
Pickers03 HEX, RGB and HSL boxes that follow each otherCustom Color Picker: 9 JS Parts, No Library