GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

트리맵 차트 9종 — 정사각형으로 쪼개는 배치부터 값 따라 흐르는 재배치까지

트리맵 차트는 칸을 어떻게 나눌까 — 01번은 줄에 칸을 더할 때마다 가장 길쭉한 칸의 비를 재서 정사각형에 가깝게 닫고, 02번은 값이 바뀌면 칸이 스프링으로 새 자리를 찾으며, 06번은 칸 크기를 재서 글자를 줄이거나 감춘다.

트리맵 차트는 넓이를 읽는 그림이 아니라 넓이를 나누는 계산이다 — 트리맵 만들기는 값 목록을 사각형 하나에 남김없이 채우는 데서 시작한다

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 장면이 함께 쓰는 설계는 하나다. 칸의 자리는 그리는 코드가 정하지 않고 squarify·slice 가 돌려준 사각형 목록이 정하며, 화면은 그 목록과 장면 시계만 읽어 그린다. 라이브러리·WebGL·CDN 은 쓰지 않고 난수는 시드를 고정한 생성기 하나뿐이라 같은 시드는 같은 판을 만든다.

값 목록이 칸의 넓이가 되는 방식

트리맵이란 값 목록을 사각형 하나에 빈틈없이 채우고 칸의 넓이를 값에 비례시키는 차트다. 값은 BASE 에 큰 순서로 24개가 들어 있고 장면마다 앞쪽 몇 개를 잘라 쓰며, 이름은 영상·사진·음악 같은 가상 항목이라 실제 회사나 서비스 이름은 없다.

배치 함수가 계산하고 화면은 그 결과만 그린다

slice 는 값 배열과 사각형 하나를 받아 칸마다 { x, y, w, h } 를 돌려주는 순수 함수다. squarify 도 같은 모양이라 그리기 코드는 이 사각형을 칠하고 글자를 얹을 뿐 넓이를 직접 계산하지 않는다.

    // 한 방향 슬라이스 — 줄을 나누지 않고 입력 순서대로 세로 띠로만 자른다
    function slice(vals, R) {
        let total = 0;
        for (const v of vals) {
            total += Math.max(v, 1e-6);
        }
        let x = R.x;
        return vals.map(function (v) {
            const w = R.w * Math.max(v, 1e-6) / total;
            const r = { x: x, y: R.y, w: w, h: R.h };
            x += w;
            return r;
        });
    }

트리맵 차트 9종 — 정사각형에 가깝게 쪼개는 줄 닫기·값이 바뀌면 스프링으로 가는 재배치·칸을 눌러 들어가는 드릴다운·증감률로 칠하는 발산 색·머리글과 여백으로 나눈 중첩·크기를 재서 맞추는 라벨·순위가 엇갈릴 때 자리 바꿈·사각과 원 비교 변형·폴더 용량 히어로

순서는 배치에서 소개 화면으로 간다. 01·02 는 배치와 재배치, 03~05 는 층과 색, 06·07 은 글자와 순위, 08·09 는 변형과 소개 화면이다.

렌더로 잰 아홉 장면의 면적·프레임·강도

장면 누적면적 움직인 프레임 강도 평균
01 줄 닫기 44.1% 19/23 73.9
02 스프링 재배치 41.6% 21/23 44.0
03 드릴다운 48.0% 15/23 45.4
04 발산 색 22.4% 23/23 43.0
05 중첩 여백 39.5% 18/23 19.0
06 라벨 맞춤 38.7% 22/23 53.2
07 순위 교차 44.2% 23/23 61.6
08 원 변형 26.5% 12/23 32.8
09 디스크 히어로 54.6% 12/23 65.7

가장 넓은 장면은 54.6% 인 09 이고 가장 좁은 장면은 22.4% 인 04 이며, 강도 평균은 01 이 73.9 로 가장 높고 05 가 19.0 으로 가장 낮다. 04 와 07 은 23/23 이라 매 프레임 움직였고, 08 과 09 는 12/23 이라 절반 가까운 프레임에서 화면이 멈춰 있다.

01길쭉한 띠 대신 정사각형에 가깝게 쪼개는 배치

디스크 용량이나 예산 구성을 칸으로 보일 때 칸이 길쭉해지면 작은 값이 눈에 들어오지 않는다. 왼쪽은 squarify 가 줄에 칸을 더할 때마다 worstRow 로 가장 길쭉한 칸의 비를 다시 재고 비가 나빠지면 줄을 닫으며, 오른쪽 slice 는 줄 없이 세로 띠로만 자른다. 칸 12개에서 최악 가로세로 비는 왼쪽 1.58, 오른쪽 28.73 이다.

길쭉한 띠 대신 정사각형에 가깝게 쪼개는 배치
새 창에서 열기

01 · 길쭉한 띠 대신 정사각형에 가깝게 쪼개는 배치 — 측정 누적면적 44.126% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

worstRowsquarifyslice
    function worstRow(areas, side) {
        let s = 0;
        let mx = 0;
        let mn = Infinity;
        for (const a of areas) {
            s += a;
            if (a > mx) {
                mx = a;
            }
            if (a < mn) {
                mn = a;
            }
        }
        const s2 = s * s;
        const d2 = side * side;
        return Math.max(d2 * mx / s2, s2 / (d2 * mn));
    }

02값이 바뀌면 칸이 스프링으로 새 자리를 찾아가는 재배치

값이 실시간으로 바뀌는 대시보드에서 칸이 순간이동하면 어느 칸이 어디로 갔는지 눈이 놓친다. springStep 이 칸 네 변을 각각 감쇠 스프링으로 목표에 끌어당기고, 목표는 값이 바뀐 순간에만 relayout 이 다시 계산하며 오른쪽 판은 목표를 그대로 그려 칸이 튄다. 문서 칸 값이 2.6배가 된 한 번 뒤 모든 칸이 목표 ±0.5px 안에 들어와 머무는 시각은 1.45초이고 최대 오버슈트는 24.0px 이다.

값이 바뀌면 칸이 스프링으로 새 자리를 찾아가는 재배치
새 창에서 열기

02 · 값이 바뀌면 칸이 스프링으로 새 자리를 찾아가는 재배치 — 측정 누적면적 41.64% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPRING_KspringSteprelayout
    function springStep(cur, vel, tgt, k, zeta, dt) {
        const c = 2 * zeta * Math.sqrt(k);
        const n = Math.max(1, Math.ceil(dt * 120));
        const h = dt / n;
        for (let s = 0; s < n; s += 1) {
            for (let j = 0; j < cur.length; j += 1) {
                vel[j] += (-k * (cur[j] - tgt[j]) - c * vel[j]) * h;
                cur[j] += vel[j] * h;
            }
        }
    }

03칸을 누르면 안쪽 하위 칸으로 확대되는 드릴다운

폴더나 카테고리처럼 층이 있는 데이터를 탐색할 때 칸을 고르면 그 칸이 화면 전체로 자라는 것이 드릴다운이다. viewRect(p) 가 보는 사각형을 그룹 사각형까지 보간하고 toScreen 이 월드 좌표를 그 사각형 기준 화면 좌표로 옮기며, 확대가 끝나면 kidLayout 이 같은 squarify 로 안쪽 칸을 채운다. 첫 그룹은 가로 ×1.92·세로 ×1.65 로, 세 번째 그룹은 가로 ×2.08·세로 ×2.60 으로 펴져 비율이 다른 그룹은 비균등하게 확대된다.

칸을 누르면 안쪽 하위 칸으로 확대되는 드릴다운
새 창에서 열기

03 · 칸을 누르면 안쪽 하위 칸으로 확대되는 드릴다운 — 측정 누적면적 48.039% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ZOOM_TviewRecttoScreen
    function viewRect(p) {
        if (zg < 0) {
            return PANEL_L;
        }
        const g = rootRects[zg];
        return { x: lerp(PANEL_L.x, g.x, p), y: lerp(PANEL_L.y, g.y, p), w: lerp(PANEL_L.w, g.w, p), h: lerp(PANEL_L.h, g.h, p) };
    }
    function toScreen(r, v) {
        const sx = PANEL_L.w / v.w;
        const sy = PANEL_L.h / v.h;
        return { x: PANEL_L.x + (r.x - v.x) * sx, y: PANEL_L.y + (r.y - v.y) * sy, w: r.w * sx, h: r.h * sy };
    }

04증감률 하나로 칠하는 파스텔 발산 색

오르내림을 한눈에 보이는 시세·점수판에서는 넓이는 값이, 색은 증감률이 맡아야 한 칸이 두 가지를 따로 말한다. divergeColor(d) 는 절댓값이 0.12 이하이면 중립색으로 두고 넘으면 ease 로 장미색(음)이나 민트색(양)까지 보간하며, 오른쪽 valueColor 는 값에서 색을 뽑아 같은 정보를 두 번 말한다. 4세트 모두 0.12 이하인 칸이 4개씩이고 오름·내림 칸 수는 6·4, 5·5, 6·4, 5·5 다.

증감률 하나로 칠하는 파스텔 발산 색
새 창에서 열기

04 · 증감률 하나로 칠하는 파스텔 발산 색 — 측정 누적면적 22.384% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRAY_NEAR_ZEROdivergeColorDELTA_SETS
    function divergeColor(d) {
        const a = clamp(Math.abs(d), 0, 1);
        const k = a <= GRAY_NEAR_ZERO ? 0 : ease((a - GRAY_NEAR_ZERO) / (1 - GRAY_NEAR_ZERO));
        return mix(NEUTRAL, d < 0 ? ROSE : MINT, k);
    }
    // 대비판 — 색을 증감률이 아닌 값에서 뽑는다(큰 값일수록 진한 라벤더)
    function valueColor(v) { return mix([236, 232, 250], [184, 170, 236], clamp(v / BASE[0], 0, 1)); }
    function signed(d) { const p = Math.round(d * 100); return (p > 0 ? '+' : '') + p + '%'; }

05그룹 머리글과 안쪽 여백으로 층을 나누는 중첩 칸

부서·팀·개인처럼 층이 겹친 구성은 머리글과 여백이 없으면 층의 경계가 보이지 않는다. layNode 가 머리글 띠를 떼고 padAt 의 여백만큼 줄인 사각형에서 자식을 다시 squarify 하며, 왼쪽은 깊이마다 여백에 0.55 를 곱해 줄이고 오른쪽은 모든 층에 같은 여백을 준다. 여백 8px 에서 잎이 판 넓이에서 차지하는 비율은 왼쪽 40.1%·오른쪽 19.1% 이고 가장 작은 잎의 짧은 변은 5.2px 대 0px 이다.

그룹 머리글과 안쪽 여백으로 층을 나누는 중첩 칸
새 창에서 열기

05 · 그룹 머리글과 안쪽 여백으로 층을 나누는 중첩 칸 — 측정 누적면적 39.463% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HEADER_HPAD_DECAYlayNode
    function padAt(depth, flat) { return flat ? PAD : PAD * Math.pow(PAD_DECAY, depth); }
    // 노드 n 을 사각형 R 에 놓는다. 머리글(깊이 1·2)을 떼고 안쪽 여백만큼 줄인 사각형에서 자식을 다시 배치
    function layNode(n, R, depth, flat) {
        const b = { r: R, inner: R, depth: depth, name: n.name, v: n.v, at: 0, kids: [] };
        seen[depth] += 1;
        b.at = depth === 0 ? 0 : (depth === 1 ? 0.4 + (seen[1] - 1) * 0.15 : (depth === 2 ? 1.5 + (seen[2] - 1) * 0.08 : 2.8 + (seen[3] - 1) * 0.03));
        if (n.kids.length === 0) {
            return b;
        }
        const p = padAt(depth, flat);
        const head = depth === 0 ? 0 : HEADER_H;
        const innerR = { x: R.x + p, y: R.y + head + (depth === 0 ? p : 0), w: Math.max(0, R.w - 2 * p), h: Math.max(0, R.h - head - p - (depth === 0 ? p : 0)) };
        b.inner = innerR;
        const lay = squarify(n.kids.map(function (k) { return k.v; }), innerR);
        b.kids = n.kids.map(function (k, i) { return layNode(k, lay.rects[i], depth + 1, flat); });
        return b;
    }

06칸 크기를 재서 글자를 줄이거나 감추는 라벨 맞춤

작은 칸이 많은 차트에서는 이름이 칸보다 길어 이웃 칸으로 넘친다. fitPx 가 estW 로 글자 폭을 어림해 15px 에서 2px 씩 내리며 칸에 들어가는 가장 큰 크기를 찾고 8px 아래면 0 을 돌려 이름을 감추지만, 오른쪽은 12px 로 고정이라 넘침이 이웃 위에 그대로 보인다. 판 폭을 0.25배로 줄이면 14칸 중 5칸은 이름을 감추고 12px 고정이면 10칸이 넘친다.

칸 크기를 재서 글자를 줄이거나 감추는 라벨 맞춤
새 창에서 열기

06 · 칸 크기를 재서 글자를 줄이거나 감추는 라벨 맞춤 — 측정 누적면적 38.724% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CHAR_WFONT_STEPHIDE_MIN
    function fitPx(s, r) {
        for (let px = FONT_MAX; px >= HIDE_MIN; px -= FONT_STEP) {
            if (estW(s, px) <= r.w - 6 && px * 1.3 <= r.h - 4) {
                return px;
            }
        }
        return 0;
    }

07값이 흘러 순위가 엇갈릴 때 자리를 바꾸는 칸

실시간 순위가 있는 점유율·방문 수 판은 값이 조금 흐를 때마다 배치를 다시 계산하면 칸이 떤다. relayoutL 은 orderKey 가 바뀌거나 값이 마지막 배치 값에서 20% 넘게 벌어진 프레임에만 불려, 순위가 엇갈리는 두 칸이 스프링으로 서로 지나가며 자리를 바꾼다. 한 판 약 9.8초 동안 재계산은 왼쪽 31번, 오른쪽 527번이다.

값이 흘러 순위가 엇갈릴 때 자리를 바꾸는 칸
새 창에서 열기

07 · 값이 흘러 순위가 엇갈릴 때 자리를 바꾸는 칸 — 측정 누적면적 44.235% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LCG_STEPorderKeyRANK_SPRING_K
        let apart = false;
        for (let i = 0; i < CELL_N; i += 1) {
            if (Math.abs(val[i] - usedVals[i]) > usedVals[i] * 0.2) {
                apart = true;
            }
        }
        if (apart || orderKey(val) !== usedKey) {
            relayoutL();
        }

08같은 데이터를 사각 칸과 원으로 오가며 비교하는 변형

차트 종류에 따라 크기의 인상이 달라지는 것은 같은 데이터를 사각 칸과 원으로 오가며 보이면 바로 드러난다. 왼쪽 반지름은 r = √(넓이 × FILL / π) 로 칸 넓이를 지키고, 오른쪽은 지름을 값에 비례시키며, pushApart 가 겹친 원을 80번 밀어낸다. 원 넓이 합은 판의 60.0% 대 42.4% 이고 최대 대 최소 넓이는 왼쪽 약 6배·오른쪽 약 32배다.

같은 데이터를 사각 칸과 원으로 오가며 비교하는 변형
새 창에서 열기

08 · 같은 데이터를 사각 칸과 원으로 오가며 비교하는 변형 — 측정 누적면적 26.478% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FILLpushApartPUSH_ROUNDS
        rectsL = squarify(vals, PANEL_L).rects;
        rectsR = squarify(vals, PANEL_R).rects;
        const radL = rectsL.map(function (r) { return Math.sqrt(r.w * r.h * FILL / Math.PI); });
        // 오른쪽은 가장 큰 칸 지름을 왼쪽 가장 큰 원보다 10% 키우고 나머지를 값에 비례
        const rBig = radL[0] * 1.1;
        const radR = vals.map(function (v) { return rBig * v / vals[0]; });
        ballsL = pushApart(rectsL, radL, PANEL_L);
        ballsR = pushApart(rectsR, radR, PANEL_R);

09폴더별 용량으로 채워지는 디스크 사용량 히어로

저장 공간·예산·트래픽 구성을 한 판에 보이는 소개 화면은 표 한 장이 칸으로 바뀌는 순간이 첫인상이다. 폴더 이름과 용량 표가 squarify 를 한 번 거쳐 칸이 되고 가장 큰 폴더(영상 220 GB)로 들어갔다 나오며, sizeCheck 가 칸 넓이 합을 판 넓이로 나눠 용량 합에 곱한 값이 합계와 같은지 검산한다. 폴더 8개의 합은 730 GB 이고 판에는 Σ 730 = 730 ✓ 가 뜬다.

폴더별 용량으로 채워지는 디스크 사용량 히어로
새 창에서 열기

09 · 폴더별 용량으로 채워지는 디스크 사용량 히어로 — 측정 누적면적 54.558% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

sizeCheckMORPH_GAPviewRect
    function sizeCheck() {
        // 칸 넓이(픽셀)의 합을 판 넓이로 나눠 용량 합에 곱한 값이 합계와 같은지 맞춰 본다
        let area = 0;
        let want = diskTotal;
        if (inside()) {
            for (const r of kidRects()) {
                area += r.w * r.h;
            }
            want = disk[zg].v;
        }
        else {
            for (const r of tgt) {
                area += r.w * r.h;
            }
        }
        const got = area / (R9.w * R9.h) * want;
        return 'Σ ' + want + ' = ' + Math.round(got) + (Math.abs(got - want) < 0.5 ? ' ✓' : ' ✗');
    }

트리맵 그리기를 코드로 짜면 칸은 어떤 값 몇 개로 정해질까?

칸 하나는 값 하나와 사각형 하나다

칸 하나는 값 하나, 배치 함수가 돌려준 사각형 하나, 그 사각형의 네 변으로 나뉜다. 네 변을 따로 두면 스프링이 왼·위·오른·아래를 각각 목표에 끌어당겨 칸이 늘고 줄며 움직인다.

값 뜻 쓰는 함수
vals 칸마다의 값. 넓이가 이 값에 비례한다 squarify·slice
사각형 { x, y, w, h } 배치 함수가 칸에 준 자리 squarify·slice
edgesOf(r) 왼·위·오른·아래 좌표 네 개 springStep
kids 칸 안에 들어 있는 하위 칸 목록 layNode·kidLayout

합이 정해진 전체를 어디에 붙이나

트리맵 그래프는 디스크 용량·예산 구성·포트폴리오 비중처럼 합이 정해진 전체를 나눠 보일 때 어울린다. 09 의 sizeCheck 는 칸 넓이 합이 용량 합과 같은지 매번 맞춰 보므로 값 목록만 바꿔도 검산이 따라온다.

대시보드 칸으로 쓸 때는 02 와 07 의 목표 계산을 값이 바뀐 순간으로 한정하는 방식이 그대로 맞는다. 층이 있는 탐색 화면이면 03 의 확대 변환과 05 의 머리글·여백을 이어 붙이면 된다.

값을 큰 순서로 세우고, 줄에 칸을 더하며 비를 재고, 줄을 닫아 사각형을 나누고, 칸 안에서 다시 같은 계산을 거듭하기까지 — 트리맵 한 판이 계산되는 순서

줄에 칸을 더하다 비가 나빠지면 줄을 닫는다

squarify 는 idx.sort 로 값을 큰 순서로 세워 두고 칸을 하나씩 더하며 worstRow 를 다시 잰다. 새 비 wr 가 지금 줄의 cur 보다 나빠지면 그 칸은 다음 줄로 넘긴다.

        while (at < n) {
            const side = Math.max(Math.min(rw, rh), 1e-6);
            const row = [];
            const areas = [];
            let cur = Infinity;
            while (at < n) {
                const a = Math.max(vals[idx[at]], 1e-6) * k;
                const next = areas.slice();
                next.push(a);
                const wr = worstRow(next, side);
                if (areas.length > 0 && wr > cur) {
                    break;
                }
                areas.push(a);
                row.push(idx[at]);
                cur = wr;
                at += 1;
            }

닫힌 줄은 남은 사각형이 가로로 길면 왼쪽에 세로 줄로, 아니면 위쪽에 가로 줄로 놓이고 남은 사각형으로 같은 일을 되풀이한다.

칸 안에서 같은 계산을 거듭한다

05 의 layNode 는 자식을 squarify 로 놓은 뒤 각 자식에서 layNode 를 다시 부르고, 03 의 kidLayout 은 확대가 끝난 뒤 그룹 안쪽 칸에 같은 함수를 쓴다. 그래서 층이 몇 개든 배치 함수는 하나다.

트리맵 차트가 깨지는 순간 — 슬라이스로만 잘라 가늘어진 칸·층마다 같은 여백으로 사라진 안쪽 칸·크기를 재지 않아 이웃으로 넘친 글자

슬라이스로만 자르면 칸이 가늘어진다

slice 는 입력 순서대로 세로 띠로만 자르므로 칸 12개의 최악 가로세로 비가 28.73 이다. 판 모서리에 찍히는 숫자도 이 최악 비이고, 평균은 노드로 따로 잰 값이라 슬라이스는 14.76 이다. 같은 값을 squarify 로 나누면 최악 1.58·평균 1.24 라서 작은 칸도 띠가 아니라 칸으로 남는다.

층마다 같은 여백을 주면 안쪽 칸이 사라진다

05 의 오른쪽 판은 모든 층에 8px 을 주기 때문에 깊은 칸은 여백만 빼고 나면 남는 폭이 0px 에 가까워진다. 짧은 변이 5px 이상인 보이는 칸은 잎 41개 가운데 38개이고 가장 작은 잎의 짧은 변은 0px 이다.

크기를 재지 않으면 글자가 이웃으로 넘친다

06 은 판 폭을 0.45배로 줄이면 14칸 중 2칸이 이름을 감추고 12px 고정에서는 7칸이 넘친다. 이 개수는 한국어 이름 기준이며, CHAR_W = 1 로 글자 폭을 한 글자에 1em 으로 어림하므로 영어나 일본어 이름에서는 개수가 달라질 수 있다.

목표를 매 프레임 다시 계산하면 칸이 떤다

07 의 오른쪽 판은 update 마다 값에 ±2.5% 잡음을 얹어 배치를 새로 계산해 한 판에 527번을 다시 계산하고, 왼쪽은 순위가 바뀌거나 값이 20% 넘게 벌어질 때만 31번 계산한다. 같은 값이 흘러도 왼쪽 칸은 안정되고 오른쪽 칸은 계속 떤다.

아홉 장면의 소스를 담은 zip 은 아래 카드에서 받고, 압축을 풀 때 쓰는 암호는 5jqmrgdv 이다.

칸 수와 스프링 세기와 여백을 바꾸면 모양과 읽힘은 얼마나 달라질까 — 칸 수·스프링·여백을 바꿔 본 차이

이름 바꾼 값 느낌
칸을 더 많이 01 CELL_N 12 → 24 최악 비는 squarify 가 1.58 에서 2.08 로, slice 가 28.73 에서 159.06 으로 벌어지고 줄은 7개에서 12개가 된다
스프링을 더 단단히 02 SPRING_K 60 → 140 ±0.5px 안에 들어와 머무는 시각이 1.45초에서 0.93초로 줄지만 오버슈트는 24.0px 에서 23.5px 로 거의 그대로다. 감쇠비가 0.5 로 고정이라 닿는 시간만 준다
여백을 더 두껍게 05 PAD 8 → 12 잎 넓이 합은 왼쪽 40.1% 에서 28.7%, 오른쪽 19.1% 에서 5.8% 가 되고 보이는 칸은 왼쪽 41개에서 38개, 오른쪽 38개에서 21개가 된다

값을 바꾸기 전에 볼 것

SPRING_K 를 올리면 목표에 닿는 시간은 줄지만 넘어가는 폭은 감쇠비 ZETA 가 정하므로, 덜 출렁이게 하려면 ZETA 쪽을 올려야 한다. 02 는 0.5, 07 은 0.6 을 쓴다.

PAD 는 깊이마다 0.55 배로 줄어드는 값이라 12px 로 올리면 가장 깊은 층까지 영향이 퍼진다. 위 수치는 판 222×208 의 첫 판 데이터에 묶여 있어 판 크기를 바꾸면 비와 넓이 비율도 달라진다.

움직임 줄이기에서는 배치가 끝난 한 장 — 칸 이름과 비율을 문장으로 읽어 주고 키보드로 칸을 고르는 접근성

방향키로 칸을 고르고 Enter 로 누른다

무대 상자(role="application", tabindex 0)에 포커스를 두면 ← ↑ 는 이전 칸, → ↓ 는 다음 칸이고 Enter·Space 가 그 칸을 고른다. P 는 일시정지, N 은 한 걸음, R 은 다시이며 처음 방향키를 누르면 자동 시연이 멈추고 직접 하기로 넘어간다.

상태 줄은 시연 중에는 조용하다

칸을 가리키면 상태 줄이 그 칸의 이름·값·비율을 문장으로 읽는다. 시연 중에는 aria-live="off" 라 읽지 않다가 처음 손대면 polite 로 바뀌고, 캔버스는 role="img" 와 항목별 설명을 갖는다. 한 판이 끝나기 0.6초 전에 숨은 role="status" 가 statusText() 를 한 번만 읽는다.

움직임 줄이기와 숨은 탭

움직임 줄이기에서는 루프 없이 REST_S 초 지점의 배치가 끝난 한 장을 그리고, 입력이 들어오면 settleStill() 이 3초치 update 를 돌려 트윈 없이 끝 배치를 곧바로 그린다. 탭이 숨으면 멈췄다가 돌아오면 시계를 다시 맞춘다.

글자 대비

칸 파스텔 면 위 진한 잉크(INK)는 최소 5.9:1 이고 머리글 띠 위는 5.2:1, 보조 잉크(INK2)는 최소 4.9:1 이다. 판 바닥 위 INK 는 11.3:1, 알약의 글자는 14.0:1 이다.

트리맵 차트 FAQ — 라이브러리 없이 만들기·트리맵 그래프가 파이 차트와 어떻게 다른지·값이 음수이거나 0 일 때 처리

트리맵 차트 만들기에 라이브러리가 필요한가요

캔버스 2D 만 있으면 되고 이 편의 아홉 장면은 라이브러리·WebGL·CDN 을 쓰지 않는다. 배치는 squarify 한 함수이고 난수는 시드를 고정한 생성기 하나라, 같은 입력은 같은 판을 만든다.

트리맵 그래프는 파이 차트와 어떻게 다른가요

파이 차트는 각도로 비율을 보이고 트리맵은 사각형 넓이로 보이므로, 칸이 많거나 층이 있는 데이터는 트리맵이 한 판에 담는다. 08 은 같은 데이터를 사각 칸과 원으로 오가며 보여 줘서 넓이로 읽을 때와 지름으로 읽을 때 큰 값이 얼마나 다르게 보이는지 비교한다.

값이 음수이거나 0 이면 어떻게 처리하나요

squarify 와 slice 는 값을 Math.max(v, 1e-6) 으로 올려 넓이가 0 인 칸이 생기지 않게 한다. 음수는 넓이로 나타낼 수 없으므로 04 처럼 증감은 색으로 따로 보이는 편이 맞다.

참고로 MDN 의 measureText 문서는 글자 폭을 재는 방법을, requestAnimationFrame 문서와 Canvas 기본 애니메이션 문서는 그리기 루프를, prefers-reduced-motion 문서는 움직임 줄이기 설정을 다룬다. 같은 계산 방식의 차트는 막대 차트 편과 파이 차트 편에서, 칸을 쌓는 배치는 벽돌 레이아웃 편과 조직도 편에서 볼 수 있다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.