ワッフルチャートライブラリ比較

3つの実装で同じ割合(サイトを訪れた100人の端末)をマス目で描き、「100人中64人」と内訳を切り替えて比較

このページでは、Mantine Charts の WaffleChart・Nivo の @nivo/waffle・自前の SVG を使ったワッフルチャート(Waffle Chart)の実装パターンを、サイトを訪れた100人の端末(スマホ 64人・PC 31人・タブレット 5人)という同じデータのインタラクティブなデモとコードで確認できます。どのデモも1マスが1人の 10×10 で、ボタンで「100人中64人」の割合と3つの内訳を切り替えられます。人の形のアイコン(ピクトグラム)は、Nivo の cellComponent と自前の SVG で描きます。

円グラフが割合を角度で見せるのに対し、ワッフルチャートは数えられるマスで見せる図です。「100人中64人」のように、割合を人数や回数として伝えたいときに向きます。円グラフ(Pie Chart)の実装比較 →

ApexCharts 8 にも、マス目や人の形を並べる unit チャート(chart.type: 'unit')がありますが、有料(Premium)の機能です。ライセンスが無いと、グラフに透かしが入り、コンソールに警告が出るので、このページでは扱っていません。

1. @mantine/charts (WaffleChart)

Mantine 9.6 で加わった WaffleChart。data に { name, value, color } を渡すと、値に比例した数のマスを塗る。total を値の合計より大きくすると、残りが空のマスになる。マスは SVG の rect で、凡例は HTML。

✓ data を渡すだけ✓ 凡例の hover で強調✓ Mantine のテーマに揃う

チャートを読み込み中...

操作:ボタンで割合/内訳を切り替え/マスに hover でその端末の人数を表示/凡例に hover でその端末のマスを強調

特徴: 幅は size(px)で決まり、省くと1マス 20px になる。親の幅に合わせては縮まないので、このデモでは @mantine/hooks の useElementSize で親の幅を測って渡している。マスの数は、値の比で割り振ったうえで端数を大きい順に足す(最大剰余法)ので、合計がマスの数とずれない。塗る向き(fillDirection)は left-to-right(既定)・right-to-left・top-to-bottom・bottom-to-top から選べる。ツールチップ(withTooltip)はポインターについて動く Mantine の Tooltip.Floating で、getTooltipLabel には data の1件と、割り当てられたマスの数(count)が渡る。マスの形は四角だけで、人の形などには変えられない。SVG には role="img" と、「スマホ: 64 of 100」のような英語の aria-label が自動で付く。 配色はMantine独自のカラースキーム(MantineProvider)で決まり、サイトのダークモード切替とは連動しないため、追従しません(ライト表示で固定しています)。
tsx
'use client';
// MantineWaffle.tsx(@mantine/core・@mantine/hooks・@mantine/charts 9.7)
import { useState } from 'react';
import { MantineProvider } from '@mantine/core';
import { WaffleChart } from '@mantine/charts';
import { useElementSize } from '@mantine/hooks';
import '@mantine/core/styles.css';
import '@mantine/charts/styles.css';

// サンプルデータ:サイトを訪れた100人の端末。1マス=1人の 10×10。並びはマスを塗る順
const DEVICES = [
  { name: 'スマホ', value: 64, color: '#3987e5' },
  { name: 'PC', value: 31, color: '#d95926' },
  { name: 'タブレット', value: 5, color: '#199e70' },
];
const TOTAL = 100;

export default function MantineWaffle() {
  const [mode, setMode] = useState<'share' | 'breakdown'>('share');
  // WaffleChart の幅は size(px)で固定され、親の幅に合わせて縮まない。親の幅を測って渡す
  const { ref, width } = useElementSize();
  // 割合はスマホの1件だけを渡し、total で残りの36人を空のマスにする
  const data = mode === 'share' ? DEVICES.slice(0, 1) : DEVICES;

  return (
    <MantineProvider>
      {(['share', 'breakdown'] as const).map((m) => (
        <button key={m} type="button" onClick={() => setMode(m)} style={{ marginRight: 4, fontWeight: mode === m ? 700 : 400 }}>{m === 'share' ? '割合' : '内訳'}</button>
      ))}
      <p style={{ fontWeight: 600 }}>{mode === 'share' ? '100人中 64人がスマホ' : '100人の端末の内訳'}</p>
      <div ref={ref} style={{ maxWidth: 320 }}>
        {width > 0 && (
          <WaffleChart
            data={data}
            rows={10}
            columns={10}
            total={TOTAL}
            size={width}
            gap={3}
            cellRadius={3}
            // 左上から右へ、1行ずつ下へ塗る(既定)。ほかに right-to-left・top-to-bottom・bottom-to-top
            fillDirection="left-to-right"
            withTooltip
            // data の1件と、割り当てられたマスの数(count)が渡る
            getTooltipLabel={(cell) => `${cell.name} ${cell.value}人(${Math.round((cell.value / TOTAL) * 100)}%)`}
            legendPosition="bottom"
          />
        )}
      </div>
    </MantineProvider>
  );
}

AIプロンプトテンプレート

React + TypeScript で、@mantine/charts(9.7)の WaffleChart を使ったワッフルチャートを実装してください。

- データ:サイトを訪れた100人の端末(スマホ 64人・PC 31人・タブレット 5人)。1マスを1人にした 10×10 で、色は端末ごとに固定すること(スマホ #3987e5・PC #d95926・タブレット #199e70)
- 切り替え:ボタンで「割合」(スマホだけを塗り、残りの36人を空のマスにする。見出しは「100人中 64人がスマホ」)と「内訳」(3つを色分けする)を切り替えること。マスは左上から右へ、1行ずつ下へ塗ること
- データ: data に { name, value, color } の配列を渡し、rows={10}・columns={10}・total={100} にすること。割合ではスマホの1件だけを渡し、total で残りを空のマスにすること
- 幅: WaffleChart の幅は size(px)で固定され、親の幅に合わせて縮まないので、@mantine/hooks の useElementSize で親の幅を測って size に渡すこと(最大 320px)
- 見た目: gap={3}・cellRadius={3}、fillDirection は "left-to-right" にすること
- ツールチップ: withTooltip を付け、getTooltipLabel で「スマホ 64人(64%)」の形にすること
- 凡例: legendPosition="bottom" にすること(凡例に hover すると、その端末のマスだけが濃く残る)

このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。

2. Nivo (@nivo/waffle)

@nivo/waffle の Waffle(ResponsiveWaffle)。data に { id, label, value }、total・rows・columns を渡すと、1マスの値(total ÷ マスの数)で割ったぶんのマスを塗る。親の大きさに合わせて、正方形のマスを並べる。SVG のほかに、HTML 版(WaffleHtml)と Canvas 版(WaffleCanvas)もある。

✓ SVG描画✓ マスの形を差し替えられる✓ ダーク自動追従

チャートを読み込み中...

操作:ボタンで割合/内訳、マス/人の形を切り替え/マスに hover でその端末のマスを強調し、人数を表示

特徴: 塗る向き(fillDirection)の既定は 'top' で、右下のマスから左へ、1行ずつ上へ塗る。ほかの2つと同じく左上から塗るため、'bottom' にしている。割合の切り替えは、スマホ以外を hiddenIds に入れている(入れた id にはマスを割り当てないので、そのぶん空のマスになる)。motionStagger(ms)で、マスが1つずつ遅れて動く。マスの形は cellComponent で差し替えられ、渡される animatedProps(x・y・size・color)を @react-spring/web の animated の要素に渡せば、アニメーションも残る。人の形は、アイコンの path に translate と scale を掛けて描いている。hover は、端末ごとのマスをまとめた外形(areas レイヤー)で受けるので、ツールチップは端末ごとに1つになる。hover している端末を部品に渡すには React の context を使う(部品を作り直すと、マスが付け直されてアニメーションが最初からになる)。 空のマス(emptyColor)・ツールチップ・凡例の色を CSS で解決できる値で渡しているので、サイトのダークモードに追従する。
tsx
'use client';
// NivoWaffle.tsx(@nivo/waffle 0.99 / @react-spring/web 10)
import { createContext, useContext, useState } from 'react';
import { ResponsiveWaffle, isDataCell, type CellComponentProps, type Datum, type TooltipProps } from '@nivo/waffle';
import { animated, to } from '@react-spring/web';

// サンプルデータ:サイトを訪れた100人の端末。1マス=1人の 10×10。並びはマスを塗る順
const DEVICES = [
  { id: 'スマホ', label: 'スマホ', value: 64, color: '#3987e5' },
  { id: 'PC', label: 'PC', value: 31, color: '#d95926' },
  { id: 'タブレット', label: 'タブレット', value: 5, color: '#199e70' },
];
const TOTAL = 100;
// 空のマスの色。'var(--empty)' のような CSS 変数も渡せる(サイトのダークモードに合わせるとき)
const EMPTY = '#e5e7eb';
// 人の形のアイコン(24×24)。頭の円と、肩の丸い胴
const PERSON = 'M12 0.5a5 5 0 1 1 0 10a5 5 0 0 1 0-10ZM2 24v-3a7 7 0 0 1 7-7h6a7 7 0 0 1 7 7v3Z';

// hover している端末。マスの部品に context で渡す
// (部品を作り直すと、マスが付け直されてアニメーションが最初からになるので、部品は固定にする)
const Active = createContext<string | null>(null);

// hover している端末があれば、それ以外のマス(空のマスを含む)を薄くする
function useOpacity({ cell, animatedProps }: CellComponentProps<Datum>) {
  const active = useContext(Active);
  const dim = active !== null && !(isDataCell(cell) && cell.data.id === active);
  return to(animatedProps.opacity, (o) => (dim ? o * 0.3 : o));
}

// マス:既定の部品と同じ rect に、薄くする処理を足したもの。animatedProps を animated の要素に渡すと、アニメーションも残る
function SquareCell(props: CellComponentProps<Datum>) {
  const { animatedProps: a, borderRadius } = props;
  return <animated.rect x={a.x} y={a.y} width={a.size} height={a.size} rx={borderRadius} fill={a.color} opacity={useOpacity(props)} />;
}

// 人の形:アイコンをマスの位置へ動かし、マスの大きさに拡大する
function PersonCell(props: CellComponentProps<Datum>) {
  const { animatedProps: a } = props;
  return (
    <animated.path
      d={PERSON}
      fill={a.color}
      opacity={useOpacity(props)}
      transform={to([a.x, a.y, a.size], (x, y, s) => `translate(${x} ${y}) scale(${s / 24})`)}
    />
  );
}

const Tooltip = ({ data }: TooltipProps<Datum>) => (
  <div style={{ background: '#fff', border: '1px solid #e5e7eb', padding: '4px 10px', fontSize: 12, whiteSpace: 'nowrap' }}>
    <span style={{ color: data.color }}>■</span> {data.label} {data.value}人({Math.round((data.value / TOTAL) * 100)}%)
  </div>
);

export default function NivoWaffle() {
  const [mode, setMode] = useState<'share' | 'breakdown'>('share');
  const [shape, setShape] = useState<'square' | 'person'>('square');
  const [active, setActive] = useState<string | null>(null);
  // 凡例:割合では、スマホと「それ以外」(空のマス)
  const legend = mode === 'share' ? [DEVICES[0], { id: 'それ以外', value: TOTAL - DEVICES[0].value, color: EMPTY }] : DEVICES;

  return (
    <div>
      {(['share', 'breakdown'] as const).map((m) => (
        <button key={m} type="button" onClick={() => setMode(m)} style={{ marginRight: 4, fontWeight: mode === m ? 700 : 400 }}>{m === 'share' ? '割合' : '内訳'}</button>
      ))}
      {(['square', 'person'] as const).map((s) => (
        <button key={s} type="button" onClick={() => setShape(s)} style={{ marginRight: 4, fontWeight: shape === s ? 700 : 400 }}>{s === 'square' ? 'マス' : '人の形'}</button>
      ))}
      <p style={{ fontWeight: 600 }}>{mode === 'share' ? '100人中 64人がスマホ' : '100人の端末の内訳'}</p>
      <div style={{ width: '100%', maxWidth: 320, aspectRatio: '1' }}>
        <Active.Provider value={active}>
          <ResponsiveWaffle
            data={DEVICES}
            // 割合は、スマホ以外を隠す(隠した id にはマスを割り当てないので、そのぶん空のマスになる)
            hiddenIds={mode === 'share' ? ['PC', 'タブレット'] : []}
            total={TOTAL}
            rows={10}
            columns={10}
            // 左上から右へ、1行ずつ下へ塗る(既定の 'top' は、右下のマスから左へ、1行ずつ上へ塗る)
            fillDirection="bottom"
            padding={3}
            borderRadius={3}
            colors={({ id }) => DEVICES.find((d) => d.id === id)!.color}
            emptyColor={EMPTY}
            // マスが1つずつ 4ms 遅れて動く。割合と内訳を切り替えると、順に塗り替わる
            motionStagger={4}
            cellComponent={shape === 'square' ? SquareCell : PersonCell}
            tooltip={Tooltip}
            // hover は、端末ごとのマスをまとめた外形(areas レイヤー)で受ける
            onMouseEnter={(d) => setActive(String(d.id))}
            onMouseLeave={() => setActive(null)}
          />
        </Active.Provider>
      </div>
      <ul style={{ display: 'flex', gap: 16, listStyle: 'none', padding: 0, fontSize: 12 }}>
        {legend.map((d) => (
          <li key={d.id} style={{ opacity: active && active !== d.id ? 0.4 : 1 }}><span style={{ color: d.color }}>■</span> {d.id} {d.value}人</li>
        ))}
      </ul>
    </div>
  );
}

AIプロンプトテンプレート

React + TypeScript で、Nivo(@nivo/waffle 0.99)の ResponsiveWaffle を使ったワッフルチャートを実装してください。

- データ:サイトを訪れた100人の端末(スマホ 64人・PC 31人・タブレット 5人)。1マスを1人にした 10×10 で、色は端末ごとに固定すること(スマホ #3987e5・PC #d95926・タブレット #199e70)
- 切り替え:ボタンで「割合」(スマホだけを塗り、残りの36人を空のマスにする。見出しは「100人中 64人がスマホ」)と「内訳」(3つを色分けする)を切り替えること。マスは左上から右へ、1行ずつ下へ塗ること
- データ: data に { id, label, value } の配列を渡し、total={100}・rows={10}・columns={10} にすること。割合では、スマホ以外を hiddenIds に入れて空のマスにすること
- 並べ方: fillDirection="bottom"(左上から右へ、1行ずつ下へ)にすること(既定の 'top' は右下から上へ塗る)
- 見た目: padding={3}・borderRadius={3}、colors は関数で端末の色を返すこと。motionStagger={4} で、切り替えたときにマスが順に塗り替わるようにすること
- 形の切り替え: ボタンで「マス」と「人の形」を切り替え、cellComponent に、@react-spring/web の animated.rect と、24×24 の人のアイコンを animated.path で描く部品を渡すこと。人の形は animatedProps の x・y・size から transform(translate と scale)を作ること
- hover: onMouseEnter・onMouseLeave で hover している端末を覚え、React の context でマスの部品に渡して、それ以外のマスを薄くすること(部品を作り直すとアニメーションが最初からになるので、部品は固定にする)
- ツールチップ: tooltip に自前の部品を渡し、「スマホ 64人(64%)」の形にすること
- 凡例: HTML で、端末の色と人数を並べること(割合では「スマホ 64人」「それ以外 36人」)
- ダークモード: 空のマスの色(emptyColor)とツールチップの色を CSS 変数で渡して、サイトのダークモードに追従させること

このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。

3. 自前の SVG(ライブラリなし)

10×10 のマスを、SVG の rect で自分で並べる。マスの番号から行と列を計算し、端末ごとの人数だけ色を割り当てるだけなので、ライブラリを使わなくても数十行で書ける。人の形は、同じ位置にアイコンの path を置く。

✓ ライブラリ不要✓ 人の形も同じ書き方✓ ダーク自動追従

チャートを読み込み中...

操作:ボタンで割合/内訳、マス/人の形を切り替え/マスに hover(タッチではタップ)でその端末のマスを強調し、人数を表示

特徴: マスの色は CSS の transition で変え、マスの番号ごとに 6ms ずつ遅らせて、左上から順に塗り替わるようにしている(prefers-reduced-motion では切る)。viewBox を使うので、表示の幅に合わせてマスごと縮む。ツールチップは、ポインターの位置に HTML で出し、枠の左右の端では内側へ寄せている。タッチでは指を離すと pointerleave が来るので、表示を消すのはマウスのときだけにして、タップしたマスの表示を残している。svg には、日本語の aria-label(「サイトを訪れた100人のうち、64人がスマホ」)を付けた。 空のマスとツールチップの色を CSS 変数で渡しているので、サイトのダークモードに追従する。
tsx
'use client';
// SvgWaffle.tsx(ライブラリなし)
import { useRef, useState } from 'react';

// サンプルデータ:サイトを訪れた100人の端末。1マス=1人の 10×10。並びはマスを塗る順
const DEVICES = [
  { name: 'スマホ', count: 64, color: '#3987e5' },
  { name: 'PC', count: 31, color: '#d95926' },
  { name: 'タブレット', count: 5, color: '#199e70' },
];
type Device = (typeof DEVICES)[number];
const TOTAL = 100;
const COLUMNS = 10;
const CELL = 24; // 1マスの大きさ(viewBox の単位。人のアイコンと同じ 24)
const GAP = 4;
const SIZE = COLUMNS * CELL + (COLUMNS - 1) * GAP;
const EMPTY = '#e5e7eb';
// 人の形のアイコン(24×24)。頭の円と、肩の丸い胴
const PERSON = 'M12 0.5a5 5 0 1 1 0 10a5 5 0 0 1 0-10ZM2 24v-3a7 7 0 0 1 7-7h6a7 7 0 0 1 7 7v3Z';
const label = (d: Device) => `${d.name} ${d.count}人(${Math.round((d.count / TOTAL) * 100)}%)`;

export default function SvgWaffle() {
  const [mode, setMode] = useState<'share' | 'breakdown'>('share');
  const [shape, setShape] = useState<'square' | 'person'>('square');
  const [hover, setHover] = useState<{ device: Device; x: number; y: number; side: 'left' | 'center' | 'right' } | null>(null);
  const boxRef = useRef<HTMLDivElement>(null);

  // マスごとの端末(左上から右へ、1行ずつ下へ)。割合ではスマホ以外を空(null)にする
  const cells = DEVICES.flatMap((d, i) => Array.from({ length: d.count }, () => (mode === 'share' && i > 0 ? null : d)));

  // ポインターの下のマスの端末と、ツールチップの位置(枠の左右の端では、内側へ寄せる)
  const show = (device: Device | null, e: React.PointerEvent) => {
    if (!device) return setHover(null);
    const box = boxRef.current!.getBoundingClientRect();
    const x = e.clientX - box.left;
    const side = x < box.width / 3 ? 'left' : x > (box.width * 2) / 3 ? 'right' : 'center';
    setHover({ device, x, y: e.clientY - box.top, side });
  };

  return (
    <div>
      {(['share', 'breakdown'] as const).map((m) => (
        <button key={m} type="button" onClick={() => setMode(m)} style={{ marginRight: 4, fontWeight: mode === m ? 700 : 400 }}>{m === 'share' ? '割合' : '内訳'}</button>
      ))}
      {(['square', 'person'] as const).map((s) => (
        <button key={s} type="button" onClick={() => setShape(s)} style={{ marginRight: 4, fontWeight: shape === s ? 700 : 400 }}>{s === 'square' ? 'マス' : '人の形'}</button>
      ))}
      <p style={{ fontWeight: 600 }}>{mode === 'share' ? '100人中 64人がスマホ' : '100人の端末の内訳'}</p>
      <div ref={boxRef} style={{ position: 'relative', maxWidth: 320 }}>
        <svg
          viewBox={`0 0 ${SIZE} ${SIZE}`}
          width="100%"
          role="img"
          aria-label={mode === 'share' ? 'サイトを訪れた100人のうち、64人がスマホ' : 'サイトを訪れた100人の端末:スマホ 64人、PC 31人、タブレット 5人'}
          // タッチでは指を離すと pointerleave が来るので、消すのはマウスのときだけ(タップしたマスの表示を残す)
          onPointerLeave={(e) => e.pointerType === 'mouse' && setHover(null)}
        >
          {cells.map((d, i) => {
            const x = (i % COLUMNS) * (CELL + GAP);
            const y = Math.floor(i / COLUMNS) * (CELL + GAP);
            const props = {
              fill: d ? d.color : EMPTY,
              // hover している端末があれば、それ以外を薄くする
              opacity: hover && hover.device !== d ? 0.3 : 1,
              // 色は1マスずつ 6ms 遅らせて変える(割合と内訳を切り替えると、左上から順に塗り替わる)
              style: { transition: `fill 300ms ease ${i * 6}ms, opacity 150ms` },
              // Tailwind CSS v4:prefers-reduced-motion では transition を切る
              className: 'motion-reduce:transition-none!',
              onPointerEnter: (e: React.PointerEvent) => show(d, e),
              onPointerMove: (e: React.PointerEvent) => show(d, e),
            };
            return shape === 'square' ? (
              <rect key={i} x={x} y={y} width={CELL} height={CELL} rx={3} {...props} />
            ) : (
              <path key={i} d={PERSON} transform={`translate(${x} ${y})`} {...props} />
            );
          })}
        </svg>
        {hover && (
          <div
            style={{
              position: 'absolute', left: hover.x, top: hover.y - 12, pointerEvents: 'none', whiteSpace: 'nowrap',
              transform: `translate(${hover.side === 'left' ? '-12px' : hover.side === 'right' ? 'calc(-100% + 12px)' : '-50%'}, -100%)`,
              background: '#fff', border: '1px solid #e5e7eb', padding: '4px 10px', fontSize: 12,
            }}
          >
            <span style={{ color: hover.device.color }}>■</span> {label(hover.device)}
          </div>
        )}
      </div>
      <ul style={{ display: 'flex', gap: 16, listStyle: 'none', padding: 0, fontSize: 12 }}>
        {(mode === 'share' ? [DEVICES[0], { name: 'それ以外', count: TOTAL - DEVICES[0].count, color: EMPTY }] : DEVICES).map((d) => (
          <li key={d.name} style={{ opacity: hover && hover.device.name !== d.name ? 0.4 : 1 }}><span style={{ color: d.color }}>■</span> {d.name} {d.count}人</li>
        ))}
      </ul>
    </div>
  );
}

AIプロンプトテンプレート

React + TypeScript で、ライブラリを使わずに、SVG を自分で描いてワッフルチャートを実装してください。

- データ:サイトを訪れた100人の端末(スマホ 64人・PC 31人・タブレット 5人)。1マスを1人にした 10×10 で、色は端末ごとに固定すること(スマホ #3987e5・PC #d95926・タブレット #199e70)
- 切り替え:ボタンで「割合」(スマホだけを塗り、残りの36人を空のマスにする。見出しは「100人中 64人がスマホ」)と「内訳」(3つを色分けする)を切り替えること。マスは左上から右へ、1行ずつ下へ塗ること
- 描画: 1マス 24・間隔 4 の viewBox に、100個のマスを rect で並べること。width="100%" で、表示の幅に合わせて縮むようにすること(最大 320px)
- 形の切り替え: ボタンで「マス」と「人の形」を切り替え、人の形は 24×24 のアイコンの path を translate でマスの位置に置くこと
- アニメーション: マスの fill を CSS の transition で変え、マスの番号ごとに 6ms ずつ遅らせて、左上から順に塗り替わるようにすること。prefers-reduced-motion では transition を切ること
- hover: マスの onPointerEnter で端末を覚え、それ以外のマスを薄くし、ポインターの上にツールチップ(「スマホ 64人(64%)」)を出すこと。枠の左右の端では、ツールチップを内側へ寄せること。タッチではタップしたマスの表示を残すこと
- 凡例: HTML で、端末の色と人数を並べること(割合では「スマホ 64人」「それ以外 36人」)
- 読み上げ: svg に role="img" と aria-label(「サイトを訪れた100人のうち、64人がスマホ」)を付けること
- ダークモード: 空のマスとツールチップの色を CSS 変数で渡して、サイトのダークモードに追従させること

このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。

ライブラリ比較

ライブラリ書き方大きさマスの形ツールチップダークモード
MantineWaffleChartsize(px)で固定四角だけマスごと(ポインターについて動く)ライト固定
Nivo@nivo/waffle の Waffle親に合わせるcellComponent で差し替え端末ごとの外形✓ 追従
自前の SVGrect・path を並べるviewBox で縮む自由自分で書く✓ 追従

選択のポイント

  • •Mantine: すでに Mantine を使っていれば、data を渡すだけで、ツールチップと凡例(hover で強調)までそろう。幅は px で決まるので、レスポンシブにするなら親の幅を測って size に渡す。
  • •Nivo: 親の大きさに合わせて並び、切り替えのアニメーション(motionStagger)もある。cellComponent で人の形などのピクトグラムにでき、SVG で色を CSS から渡せるので、サイトのダークモードにも合わせやすい。
  • •自前の SVG: マスを並べるだけなので、ライブラリを足したくない場合に向く。形・並べ方・アニメーションを自由に決められるが、ツールチップと読み上げは自分で書く。

典型的な使用例

  • 👥アンケートや利用者の割合:「100人中64人がスマホ」「10人に3人が利用」のように、割合を人数として伝える
  • 🎯目標の達成率・進み具合:100マスのうち何マス埋まったかで、達成率を見せる(進捗バーの代わり)
  • 🗳️得票や議席の内訳:1マスを1票・1議席にして、数えられる形で見せる
  • 📰記事・レポートのインフォグラフィック:人の形のアイコンを並べて、数字を直感的に伝える

⚠️ 使用時の注意点

  • •1マスの意味(1人・1%・1,000円など)を、見出しや凡例に書く。書かないと、マスの数が何を表すか分からない
  • •マスの数は整数なので、割合は丸められる(10×10 なら 1% 単位)。細かい差を見せたいときは、棒グラフや数字を添える
  • •色分けする項目は少なめにする(3〜5 くらい)。多いと、どのマスがどの項目か見分けにくい。小さい項目は「その他」にまとめる
  • •隣り合うマスの色が似ていると、境目が分からない。隣になる項目どうしを見分けやすい色にし(このページの色は、色覚の違いでも見分けられることを確かめてある)、マスの間に隙間を空ける
  • •人の形のアイコンは、四角より塗る面が小さいので、色の違いが見えにくくなる。アイコンを大きめにするか、項目を減らす

ワッフルチャート(Waffle Chart)は、10×10 などのマス目を並べ、項目の割合に応じた数のマスを塗って、全体に対する割合を見せる図。マスを人やモノの形のアイコンにしたものは、ピクトグラム(アイソタイプ)とも呼ばれる。

アンケートや利用者の割合、目標の達成率、得票や議席の内訳など、割合を「100人中◯人」のように数えられる形で伝えたい場面で使われる。記事やレポートのインフォグラフィックにも多い。

主なバリエーション
  • •割合型(1つの項目だけを塗り、残りを空のマスにする)
  • •内訳型(項目ごとに色分けして、すべてのマスを塗る)
  • •ピクトグラム型(マスを人やモノの形のアイコンにする)
  • •ユニット型(マスの数で量を見せる。全体が決まっていない)

📊 使用しているサンプルデータ(クリックで表示)

あるサイトを訪れた100人の端末を、スマホ 64人・PC 31人・タブレット 5人として用意しました。1マスを1人にしているので、マスの数がそのまま人数(と割合)になります。3つのデモとコード例は同じ値・同じ色です。

関連する逆引きリファレンス