市場分析氣泡圖
12 顆氣泡依序以彈簧動畫從 0 縮放至目標尺寸,X 軸代表市場規模、Y 軸代表成長率、圓圈大小代表市佔率,三個產業群組以色彩區分並附有發光效果。
圖表SVG商務
提示詞(可直接修改內容)
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import React from "react";
const BUBBLES = [
// group A - 科技
{ x: 72, y: 88, size: 45, label: "AI", color: "#3b82f6", group: "科技" },
{ x: 65, y: 72, size: 32, label: "雲端", color: "#3b82f6", group: "科技" },
{ x: 80, y: 65, size: 28, label: "資安", color: "#3b82f6", group: "科技" },
{ x: 55, y: 80, size: 18, label: "AR/VR", color: "#3b82f6", group: "科技" },
// group B - 消費
{ x: 35, y: 45, size: 55, label: "電商", color: "#10b981", group: "消費" },
{ x: 42, y: 38, size: 38, label: "外送", color: "#10b981", group: "消費" },
{ x: 28, y: 55, size: 22, label: "訂閱", color: "#10b981", group: "消費" },
{ x: 48, y: 28, size: 15, label: "共享", color: "#10b981", group: "消費" },
// group C - 金融
{ x: 20, y: 62, size: 42, label: "支付", color: "#f59e0b", group: "金融" },
{ x: 30, y: 75, size: 25, label: "加密貨幣", color: "#f59e0b", group: "金融" },
{ x: 15, y: 48, size: 35, label: "保險", color: "#f59e0b", group: "金融" },
{ x: 22, y: 35, size: 20, label: "借貸", color: "#f59e0b", group: "金融" },
];
const LEGEND-GROUPS = [
{ group: "科技", color: "#3b82f6" },
{ group: "消費", color: "#10b981" },
{ group: "金融", color: "#f59e0b" },
];
// Chart area layout
const CHART-LEFT = 160;
const CHART-TOP = 180;
const CHART-RIGHT = 1760;
const CHART-BOTTOM = 960;
const CHART-W = CHART-RIGHT - CHART-LEFT;
const CHART-H = CHART-BOTTOM - CHART-TOP;
const GRID-COUNT = 4;
function bubbleRadius(size: number) {
return (size / 55) * 60 + 15;
}
function chartX(xPct: number) {
return CHART-LEFT + (xPct / 100) * CHART-W;
}
function chartY(yPct: number) {
// y=0 at bottom, y=100 at top
return CHART-BOTTOM - (yPct / 100) * CHART-H;
}
export const BubbleChart: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Title entrance
const titleProgress = spring({ frame, fps, config: { damping: 30, stiffness: 70 } });
const titleOpacity = interpolate(titleProgress, [0, 1], [0, 1]);
const titleY = interpolate(titleProgress, [0, 1], [-30, 0]);
// Grid entrance
const gridProgress = spring({ frame, fps, config: { damping: 40, stiffness: 60 } });
const gridOpacity = interpolate(gridProgress, [0, 1], [0, 0.5]);
// Axis label entrance
const axisOpacity = interpolate(gridProgress, [0, 1], [0, 1]);
return (
<AbsoluteFill
style={{
background: "#0f0f0f",
fontFamily: "sans-serif",
overflow: "hidden",
}}
>
{/* Title */}
<div
style={{
position: "absolute",
top: 56,
left: 0,
right: 0,
textAlign: "center",
opacity: titleOpacity,
transform: `translateY(${titleY}px)`,
}}
>
<div style={{ fontSize: 52, fontWeight: 700, color: "#ffffff", letterSpacing: "0.04em" }}>
市場分析氣泡圖
</div>
<div style={{ marginTop: 8, fontSize: 22, color: "#6b7280", letterSpacing: "0.06em" }}>
市場規模 × 成長率 × 市佔率
</div>
</div>
{/* SVG chart */}
<svg
width={1920}
height={1080}
style={{ position: "absolute", top: 0, left: 0 }}
viewBox="0 0 1920 1080"
>
{/* Grid lines */}
{Array.from({ length: GRID-COUNT + 1 }, (_, i) => {
const frac = i / GRID-COUNT;
const xPos = CHART-LEFT + frac * CHART-W;
const yPos = CHART-TOP + frac * CHART-H;
const label = Math.round(frac * 100);
return (
<g key={i} opacity={gridOpacity}>
{/* vertical grid */}
<line
x1={xPos} y1={CHART-TOP}
x2={xPos} y2={CHART-BOTTOM}
stroke="#374151"
strokeWidth={1}
strokeDasharray={i === 0 ? "none" : "6 4"}
/>
{/* horizontal grid */}
<line
x1={CHART-LEFT} y1={yPos}
x2={CHART-RIGHT} y2={yPos}
stroke="#374151"
strokeWidth={1}
strokeDasharray={i === GRID-COUNT ? "none" : "6 4"}
/>
{/* X axis labels */}
<text
x={xPos}
y={CHART-BOTTOM + 36}
textAnchor="middle"
fill="#6b7280"
fontSize={20}
opacity={axisOpacity}
>
{label}
</text>
{/* Y axis labels */}
<text
x={CHART-LEFT - 16}
y={CHART-BOTTOM - frac * CHART-H + 7}
textAnchor="end"
fill="#6b7280"
fontSize={20}
opacity={axisOpacity}
>
{label}
</text>
</g>
);
})}
{/* Axis titles */}
<text
x={CHART-LEFT + CHART-W / 2}
y={CHART-BOTTOM + 76}
textAnchor="middle"
fill="#9ca3af"
fontSize={24}
opacity={axisOpacity}
>
市場規模(百分位)
</text>
<text
x={CHART-LEFT - 80}
y={CHART-TOP + CHART-H / 2}
textAnchor="middle"
fill="#9ca3af"
fontSize={24}
opacity={axisOpacity}
transform={`rotate(-90, ${CHART-LEFT - 80}, ${CHART-TOP + CHART-H / 2})`}
>
成長率(百分位)
</text>
{/* Bubbles */}
{BUBBLES.map((bubble, index) => {
const startFrame = index * 8 + 15;
const bubbleProgress = spring({
frame: Math.max(0, frame - startFrame),
fps,
config: { damping: 18, stiffness: 100 },
});
const scale = interpolate(bubbleProgress, [0, 1], [0, 1]);
const opacity = interpolate(bubbleProgress, [0, 0.3, 1], [0, 1, 1], {
extrapolateRight: "clamp",
});
const cx = chartX(bubble.x);
const cy = chartY(bubble.y);
const r = bubbleRadius(bubble.size);
// Determine label position (push label right for left-side bubbles, above for high bubbles)
const labelOffsetX = bubble.x < 50 ? r + 12 : -(r + 12);
const labelAnchor = bubble.x < 50 ? "start" : "end";
return (
<g key={index} opacity={opacity}>
{/* Glow filter via drop-shadow */}
<circle
cx={cx}
cy={cy}
r={r * scale}
fill={bubble.color}
fillOpacity={0.18}
stroke={bubble.color}
strokeWidth={2}
style={{
filter: `drop-shadow(0 0 ${12 * scale}px ${bubble.color}aa)`,
}}
/>
{/* Label */}
<text
x={cx + labelOffsetX * scale}
y={cy + 6}
textAnchor={labelAnchor}
fill={bubble.color}
fontSize={r > 40 ? 22 : 18}
fontWeight={600}
>
{bubble.label}
</text>
</g>
);
})}
</svg>
{/* Legend */}
<div
style={{
position: "absolute",
bottom: 56,
right: 80,
display: "flex",
gap: 36,
opacity: axisOpacity,
}}
>
{LEGEND-GROUPS.map(({ group, color }) => (
<div key={group} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 18,
height: 18,
borderRadius: "50%",
background: color,
boxShadow: `0 0 8px ${color}88`,
}}
/>
<span style={{ color: "#d1d5db", fontSize: 22 }}>{group}</span>
</div>
))}
{/* Size legend */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginLeft: 16 }}>
<svg width={36} height={36}>
<circle cx={18} cy={18} r={14} fill="none" stroke="#6b7280" strokeWidth={2} />
<circle cx={18} cy={18} r={6} fill="none" stroke="#6b7280" strokeWidth={1.5} />
</svg>
<span style={{ color: "#6b7280", fontSize: 20 }}>圓圈大小 = 市佔率</span>
</div>
</div>
</AbsoluteFill>
);
};登入後查看完整程式碼