remotion データビジュアライゼーション 棒グラフ アニメーション react

Remotionでデータビジュアライゼーション動画を作る方法(棒グラフ・カウンター・レーシングチャート)

Remotionでデータビジュアライゼーション動画を作る方法(棒グラフ・カウンター・レーシングチャート)

アニメーション付きのデータビジュアライゼーションは、現代のビジネス動画コンテンツで最も効果的な表現手法のひとつです。顧客数が1万人にカウントアップするアニメーション、市場シェアを示す棒グラフが伸びていく映像、時系列で順位が変動するレーシングチャート——これらは静的なグラフィックでは伝えられないインパクトをデータに与えます。

Remotionではこれら3つすべてがuseCurrentFrame()を利用したシンプルなReactアニメーションとして実装できます。本記事では実際に使えるコード付きで各パターンを解説します。


なぜRemotionでデータアニメーションを作るのか

従来の動画制作では、データアニメーションはAfter Effectsのエクスプレッションや、FlourishやData Wrapperなどの専用ツールで作ります。Remotionは別のトレードオフを提供します。

  • データはコード: 数値の配列をpropsとして渡し、データを変えて再レンダリングするだけ
  • ピクセル単位の制御: ReactコンポーネントにCSSレイアウト・SVG・Canvasを自由に使える
  • 再現性: 同じデータは必ず同じ動画を生成する。自動化パイプラインが容易になる
  • 再利用性: コンポーネントを一度書けば、四半期報告・SNS投稿・クライアント向け動画に何度でも使い回せる

アニメーション付きカウンター

カウンターは最もシンプルなデータビジュアライゼーションです。ゼロ(または任意の開始値)から目標値まで数字が増加します。

import { interpolate, useCurrentFrame } from "remotion";

interface CounterProps {
  from?: number;
  to: number;
  durationInFrames?: number;
  prefix?: string;
  suffix?: string;
  decimals?: number;
}

export const AnimatedCounter: React.FC<CounterProps> = ({
  from = 0,
  to,
  durationInFrames = 60,
  prefix = "",
  suffix = "",
  decimals = 0,
}) => {
  const frame = useCurrentFrame();

  const value = interpolate(frame, [0, durationInFrames], [from, to], {
    extrapolateRight: "clamp",
    easing: (t) => 1 - Math.pow(1 - t, 3), // イーズアウト三次
  });

  return (
    <div
      style={{
        fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
        fontSize: 120,
        fontWeight: 800,
        color: "#ffffff",
        letterSpacing: -2,
        fontVariantNumeric: "tabular-nums",
      }}
    >
      {prefix}
      {value.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
      {suffix}
    </div>
  );
};

使用例

// 0から10,000まで2秒(30fps)でカウントアップ
<AnimatedCounter from={0} to={10000} durationInFrames={60} suffix=" 社" />

// 0から98.6%まで
<AnimatedCounter from={0} to={98.6} durationInFrames={45} suffix="%" decimals={1} />

fontVariantNumeric: "tabular-nums"を指定することで、桁数が変わるときに数字が左右にぶれなくなります。仕上がりの品質を高める小さながら重要な設定です。


アニメーション付き棒グラフ

棒グラフは高さをゼロから最終値まで成長させ、各棒に時間差(スタッガー)をつけて表示します。

import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

interface BarData {
  label: string;
  value: number;
  color?: string;
}

interface AnimatedBarChartProps {
  data: BarData[];
  maxValue?: number;
  chartHeight?: number;
  staggerFrames?: number;
  barColor?: string;
}

export const AnimatedBarChart: React.FC<AnimatedBarChartProps> = ({
  data,
  maxValue,
  chartHeight = 400,
  staggerFrames = 8,
  barColor = "#4f46e5",
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const computedMax = maxValue ?? Math.max(...data.map((d) => d.value));

  return (
    <div
      style={{
        display: "flex",
        alignItems: "flex-end",
        gap: 16,
        height: chartHeight,
        fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
      }}
    >
      {data.map((bar, i) => {
        const barFrame = i * staggerFrames;
        const progress = spring({
          fps,
          frame: Math.max(0, frame - barFrame),
          config: { damping: 20, stiffness: 100 },
        });

        const barHeight = (bar.value / computedMax) * chartHeight * progress;

        return (
          <div
            key={i}
            style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}
          >
            {/* バー上部の値ラベル */}
            <div
              style={{
                color: "#fff",
                fontSize: 18,
                fontWeight: 700,
                opacity: progress,
              }}
            >
              {bar.value.toLocaleString()}
            </div>
            {/* バー本体 */}
            <div
              style={{
                width: 60,
                height: barHeight,
                background: bar.color ?? barColor,
                borderRadius: "6px 6px 0 0",
              }}
            />
            {/* X軸ラベル */}
            <div style={{ color: "rgba(255,255,255,0.7)", fontSize: 14 }}>
              {bar.label}
            </div>
          </div>
        );
      })}
    </div>
  );
};

使用例

const salesData: BarData[] = [
  { label: "Q1", value: 42000 },
  { label: "Q2", value: 67000 },
  { label: "Q3", value: 91000 },
  { label: "Q4", value: 138000 },
];

<AnimatedBarChart data={salesData} barColor="#6366f1" />

レーシングバーチャート

レーシングバーチャートは、時間の経過とともに順位が変化するデータをアニメーションで表示します。SNSで最も人気のあるデータビジュアライゼーション形式のひとつです。

実装は2つの層で構成されます。「現フレームの値(スナップショット間の補間)」と「現在の順位(各バーの縦位置の決定に使用)」です。

import { interpolate, useCurrentFrame } from "remotion";

interface DataPoint {
  [key: string]: number;
}

interface RacingBarChartProps {
  snapshots: DataPoint[];  // キーフレームごとの1つのオブジェクト
  snapshotFrames: number[]; // 各スナップショットのフレーム番号
  colors: Record<string, string>;
  barHeight?: number;
}

export const RacingBarChart: React.FC<RacingBarChartProps> = ({
  snapshots,
  snapshotFrames,
  colors,
  barHeight = 52,
}) => {
  const frame = useCurrentFrame();

  const snapshotIndex = snapshotFrames.findIndex((f) => f > frame) - 1;
  const clampedIndex = Math.max(0, Math.min(snapshotIndex, snapshots.length - 2));

  const from = snapshots[clampedIndex];
  const to = snapshots[clampedIndex + 1] ?? snapshots[clampedIndex];
  const fromFrame = snapshotFrames[clampedIndex];
  const toFrame = snapshotFrames[clampedIndex + 1] ?? fromFrame + 1;

  const t = interpolate(frame, [fromFrame, toFrame], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const keys = Object.keys(from);
  const currentValues: Record<string, number> = {};
  for (const key of keys) {
    currentValues[key] = interpolate(t, [0, 1], [from[key], to[key]]);
  }

  // 値の降順でソート
  const sorted = keys.sort((a, b) => currentValues[b] - currentValues[a]);
  const maxValue = Math.max(...Object.values(currentValues));

  return (
    <div style={{ position: "relative", width: "100%", height: sorted.length * (barHeight + 12) }}>
      {sorted.map((key, rank) => {
        const value = currentValues[key];
        const barWidth = (value / maxValue) * 80;

        return (
          <div
            key={key}
            style={{
              position: "absolute",
              top: rank * (barHeight + 12),
              left: 0,
              right: 0,
              display: "flex",
              alignItems: "center",
              gap: 12,
              fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
            }}
          >
            <div style={{ width: 120, textAlign: "right", color: "#fff", fontSize: 14, fontWeight: 600 }}>
              {key}
            </div>
            <div
              style={{
                height: barHeight,
                width: `${barWidth}%`,
                background: colors[key] ?? "#4f46e5",
                borderRadius: "0 6px 6px 0",
                display: "flex",
                alignItems: "center",
                justifyContent: "flex-end",
                paddingRight: 12,
                color: "#fff",
                fontSize: 16,
                fontWeight: 700,
              }}
            >
              {Math.round(value).toLocaleString()}
            </div>
          </div>
        );
      })}
    </div>
  );
};

注意点: RemotionではCSSトランジションはフレーム間でアニメーションしません。すべてのフレームは新規レンダリングです。滑らかさはinterpolateが各フレームで値を補間することで実現されます。


3つを組み合わせた「データストーリー」

import { Sequence } from "remotion";

export const DataStory: React.FC = () => (
  <>
    {/* 見出し数字をカウントアップで見せる */}
    <Sequence from={0} durationInFrames={60}>
      <AnimatedCounter to={10000} suffix=" 社" />
    </Sequence>

    {/* 四半期別の棒グラフ */}
    <Sequence from={60} durationInFrames={90}>
      <AnimatedBarChart data={salesData} />
    </Sequence>

    {/* 12ヶ月間の順位変動をレーシングチャートで */}
    <Sequence from={150} durationInFrames={120}>
      <RacingBarChart
        snapshots={monthlyData}
        snapshotFrames={[0, 30, 60, 90, 120]}
        colors={brandColors}
      />
    </Sequence>
  </>
);

テンプレートでデータビジュアライゼーション動画を効率化する

上記のコンポーネントをゼロから書くことは開発者であれば十分可能ですが、ブロードキャスト品質に仕上げるには相当な反復作業が必要です。イージングの微調整、ラベル位置の計算、複数アスペクト比への対応、負の値や長いラベルのエッジケース処理など、地味な工数が積み重なります。

RenderCompの1,400本以上のRemotionテンプレートにはデータビジュアライゼーション系のテンプレートも含まれています。定期的な実績報告動画・投資家向けアップデート・SNS用データコンテンツを制作するチームにとって、エッジケースを処理済みの洗練されたテンプレートを起点にデータだけ差し替えるアプローチは、制作品質と速度を両立する現実的な選択肢です。ソースコードはすべて自分のものとして所有でき、細部まで自由に変更できます。


よくある質問(FAQ)

Q: D3で作ったSVGチャートをRemotionでアニメーションできますか? A: はい。D3はシェイプとパスを計算するJavaScriptライブラリで、DOM操作を必要としません。Remotionのコンポーネント内でD3のレイアウト関数を呼び出し、生成された<path><rect>をReactのSVG要素としてレンダリングし、useCurrentFrame()で値を制御できます。

Q: 棒グラフにY軸とグリッドラインを追加するには? A: 最大値から計算したY位置に水平の<div>またはSVGの<line>要素を並べます。各グリッドラインのラベルはデータ範囲から読みやすい間隔(例:2万5千単位)に丸めて表示します。

Q: CSVやAPIからデータを取り込めますか? A: レンダリング時に、RemotionのcalculateMetadata関数でNode.jsのfsを使ってJSONやCSVを読み込むか、APIからfetchできます。取得したデータはコンポーネントにinputPropsとして渡します。

Q: カテゴリのラベルが長い場合の対処法は? A: ラベルをtransform: "rotate(-45deg)"で斜めにするか、末尾を切り捨てます。カテゴリ数が多い場合は水平バーチャート(棒グラフを90度回転)に変更するとラベルが読みやすくなります。

Q: カウンターアニメーションに最適なイージングは? A: イーズアウト三次(1 - (1-t)^3)が多くの用途で機能します。最初は速く動き、最終値に近づくにつれて減速するため、視線が数字に引き寄せられます。成長マイルストーンを表すカウンターには、ダンピングを低めに設定したspringでわずかなオーバーシュートをつけると躍動感が出ます。

Q: 50以上のカテゴリでレーシングチャートは作れますか? A: 技術的には可能ですが、チャートが読めなくなります。どのフレームでも表示するバーはトップ10〜12に絞るのが実用的です。全カテゴリの値は計算しながら、表示は上位N件に限定します。

Q: LinkedIn向けにデータビジュアライゼーション動画を書き出すには? A: LinkedInはH.264のMP4、最大1920×1080、30fpsに対応しています。npx remotion render --codec=h264を使い、コンポジションにwidth={1920} height={1080}を指定します。モバイルで見やすい正方形フォーマット(1080×1080)も選択肢のひとつです。


まとめ

Remotionはデータビジュアライゼーションをプログラミングの問題として扱います。そのため、バージョン管理・ループ・型付きデータ構造など、開発者がすでに知っているツールがそのまま使えます。

本記事で紹介した3つのパターン(カウンター・棒グラフ・レーシングチャート)は、動画コンテンツにおけるデータストーリーテリングのニーズの大半をカバーします。定期的にデータ動画を制作するチームには、これらのパターンとRenderCompのようなテンプレートライブラリを組み合わせることで、データが変わっても一定の品質を維持できる制作パイプラインが構築できます。

すぐに使える

1,400以上のRemotionテンプレートを一括入手

買い切り永続ライセンス。TypeScript製。今日から動画制作スピードが変わります。

RenderCompを試す →