Remotion LabRemotion Lab
返回模板庫

水平長條競賽圖

六條水平長條依排名由上到下交錯彈出,條形寬度動態增長至目標比例,左側顯示地區名稱與排名,右側呈現即時數值,配色鮮豔並帶有發光陰影,適合地區排行榜與數據比較。

圖表商務華麗
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const ITEMS = [
  { name: "台灣", value: 9420, color: "#3b82f6" },
  { name: "日本", value: 8150, color: "#8b5cf6" },
  { name: "韓國", value: 7380, color: "#06b6d4" },
  { name: "香港", value: 6240, color: "#10b981" },
  { name: "新加坡", value: 5690, color: "#f59e0b" },
  { name: "泰國", value: 4320, color: "#ec4899" },
];

const MAX-VALUE = ITEMS[0].value;
const BAR-MAX-WIDTH-PERCENT = 70;

interface BarProps {
  name: string;
  value: number;
  color: string;
  frame: number;
  fps: number;
  startFrame: number;
  rank: number;
}

const Bar: React.FC<BarProps> = ({
  name,
  value,
  color,
  frame,
  fps,
  startFrame,
  rank,
}) => {
  const growProgress = spring({
    frame: frame - startFrame,
    fps,
    config: { damping: 24, stiffness: 100 },
  });

  const appearProgress = spring({
    frame: frame - startFrame,
    fps,
    config: { damping: 28, stiffness: 140 },
  });

  const targetWidthPercent = (value / MAX-VALUE) * BAR-MAX-WIDTH-PERCENT;
  const currentWidthPercent = interpolate(
    growProgress,
    [0, 1],
    [0, targetWidthPercent],
    { extrapolateRight: "clamp" }
  );

  const opacity = interpolate(appearProgress, [0, 0.3], [0, 1], {
    extrapolateRight: "clamp",
  });

  const x = interpolate(appearProgress, [0, 1], [-40, 0], {
    extrapolateRight: "clamp",
  });

  const displayValue = Math.floor(
    interpolate(growProgress, [0, 1], [0, value], { extrapolateRight: "clamp" })
  ).toLocaleString("zh-TW");

  return (
    <div
      style={{
        display: "flex",
        alignItems: "center",
        gap: 0,
        height: 64,
        opacity,
        transform: `translateX(${x}px)`,
      }}
    >
      {/* 排名 */}
      <div
        style={{
          width: 48,
          fontSize: 22,
          fontWeight: 700,
          color: "#4b5563",
          fontFamily: "sans-serif",
          textAlign: "right",
          paddingRight: 16,
          flexShrink: 0,
        }}
      >
        {rank}
      </div>
      {/* 地區名稱 */}
      <div
        style={{
          width: 100,
          fontSize: 24,
          fontWeight: 700,
          color: "#e5e7eb",
          fontFamily: "sans-serif",
          textAlign: "right",
          paddingRight: 24,
          flexShrink: 0,
        }}
      >
        {name}
      </div>
      {/* 長條 + 數值 */}
      <div
        style={{
          flex: 1,
          display: "flex",
          alignItems: "center",
          position: "relative",
          height: 44,
        }}
      >
        <div
          style={{
            width: `${currentWidthPercent}%`,
            height: "100%",
            background: `linear-gradient(90deg, ${color}cc, ${color})`,
            borderRadius: 8,
            flexShrink: 0,
            minWidth: 4,
            boxShadow: `0 0 16px ${color}55`,
            transition: "none",
          }}
        />
        <span
          style={{
            marginLeft: 16,
            fontSize: 22,
            fontWeight: 700,
            color: "#9ca3af",
            fontFamily: "sans-serif",
            whiteSpace: "nowrap",
          }}
        >
          {displayValue}
        </span>
      </div>
    </div>
  );
};

export const HorizontalBarRace: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill
      style={{
        background: "#0f0f0f",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: "0 120px",
        boxSizing: "border-box",
      }}
    >
      {/* 標題 */}
      <div
        style={{
          fontSize: 32,
          fontWeight: 700,
          color: "#4b5563",
          fontFamily: "sans-serif",
          letterSpacing: 4,
          marginBottom: 60,
          alignSelf: "flex-start",
          paddingLeft: 148,
        }}
      >
        各地區用戶數排行
      </div>
      {/* 長條圖 */}
      <div
        style={{
          width: "100%",
          display: "flex",
          flexDirection: "column",
          gap: 20,
        }}
      >
        {ITEMS.map((item, index) => (
          <Bar
            key={item.name}
            {...item}
            frame={frame}
            fps={fps}
            startFrame={index * 10}
            rank={index + 1}
          />
        ))}
      </div>
      {/* 單位標注 */}
      <div
        style={{
          alignSelf: "flex-end",
          marginTop: 32,
          fontSize: 18,
          color: "#374151",
          fontFamily: "sans-serif",
          letterSpacing: 1,
        }}
      >
        單位:人
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼