Розроблення теплової карти крипторинку
Теплова карта (heatmap) криптовалютного ринку — це візуалізація зміни цін усього ринку одночасно. Кожен актив представлено прямокутником, розмір якого пропорціональний капіталізації, колір відображає відсоток змін за період. Незамінний інструмент для швидкого розуміння структури ринку.
Алгоритм Treemap Layout
Основа heatmap — алгоритм Treemap, який оптимально упаковує прямокутники різних розмірів:
interface HeatmapCell {
symbol: string;
marketCap: number;
changePercent: number;
price: number;
volume24h: number;
x: number; // обчислюється алгоритмом
y: number;
width: number;
height: number;
}
class SquarifiedTreemap {
layout(
data: HeatmapCell[],
bounds: {x: number; y: number; width: number; height: number}
): HeatmapCell[] {
// Сортуємо по спаданню розміру (капіталізація)
const sorted = [...data].sort((a, b) => b.marketCap - a.marketCap);
const totalMarketCap = sorted.reduce((sum, d) => sum + d.marketCap, 0);
return this.squarify(sorted, bounds, totalMarketCap);
}
private squarify(items: HeatmapCell[], bounds: Bounds, total: number): HeatmapCell[] {
if (items.length === 0) return [];
const isHorizontal = bounds.width >= bounds.height;
const result: HeatmapCell[] = [];
let row: HeatmapCell[] = [];
let rowArea = 0;
let offset = 0;
for (const item of items) {
const itemArea = (item.marketCap / total) * (bounds.width * bounds.height);
const testRow = [...row, item];
const testArea = rowArea + itemArea;
if (this.wouldImproveAspectRatio(testRow, testArea, bounds, isHorizontal)) {
row.push(item);
rowArea = testArea;
} else {
// Завершуємо поточний рядок
result.push(...this.layoutRow(row, rowArea, bounds, isHorizontal, offset, total));
const rowThickness = rowArea / (isHorizontal ? bounds.width : bounds.height);
offset += rowThickness;
row = [item];
rowArea = itemArea;
}
}
if (row.length > 0) {
result.push(...this.layoutRow(row, rowArea, bounds, isHorizontal, offset, total));
}
return result;
}
}
Отримання даних
import httpx
import asyncio
class MarketDataProvider:
COINGECKO_URL = "https://api.coingecko.com/api/v3"
async def get_heatmap_data(
self,
vs_currency: str = 'usd',
top_n: int = 100
) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.COINGECKO_URL}/coins/markets",
params={
"vs_currency": vs_currency,
"order": "market_cap_desc",
"per_page": top_n,
"page": 1,
"sparkline": False,
"price_change_percentage": "1h,24h,7d"
}
)
coins = response.json()
return [
{
"symbol": c["symbol"].upper(),
"name": c["name"],
"market_cap": c["market_cap"] or 0,
"change_1h": c.get("price_change_percentage_1h_in_currency", 0) or 0,
"change_24h": c.get("price_change_percentage_24h", 0) or 0,
"change_7d": c.get("price_change_percentage_7d_in_currency", 0) or 0,
"volume_24h": c.get("total_volume", 0) or 0,
"price": c["current_price"],
"image": c["image"]
}
for c in coins if c["market_cap"]
]
Кешуємо дані: CoinGecko безплатний API — 10-30 запитів/хв. Оновлюємо кожні 60 секунд:
async def get_cached_data(self) -> list[dict]:
cache_key = "heatmap_data"
cached = await self.redis.get(cache_key)
if cached:
return json.loads(cached)
data = await self.get_heatmap_data()
await self.redis.setex(cache_key, 60, json.dumps(data))
return data
React візуалізація
import React, { useMemo } from 'react';
const getColor = (changePercent: number): string => {
const intensity = Math.min(Math.abs(changePercent) / 10, 1);
if (changePercent > 0) {
const green = Math.floor(180 * intensity + 60);
return `rgb(0, ${green}, 0)`;
} else {
const red = Math.floor(180 * intensity + 60);
return `rgb(${red}, 0, 0)`;
}
};
const HeatmapCell: React.FC<{cell: HeatmapCell; period: '1h'|'24h'|'7d'}> = ({cell, period}) => {
const change = period === '1h' ? cell.change1h : period === '24h' ? cell.change24h : cell.change7d;
const bgColor = getColor(change);
return (
<div
style={{
position: 'absolute',
left: cell.x,
top: cell.y,
width: cell.width - 2,
height: cell.height - 2,
backgroundColor: bgColor,
border: '1px solid rgba(0,0,0,0.3)',
overflow: 'hidden',
cursor: 'pointer'
}}
title={`${cell.symbol}: ${change > 0 ? '+' : ''}${change.toFixed(2)}%`}
>
{cell.width > 40 && (
<div className="p-1 text-white">
<div className="font-bold text-xs">{cell.symbol}</div>
{cell.height > 30 && (
<div className={`text-xs ${change > 0 ? 'text-green-200' : 'text-red-200'}`}>
{change > 0 ? '+' : ''}{change.toFixed(2)}%
</div>
)}
</div>
)}
</div>
);
};
const CryptoHeatmap: React.FC = () => {
const [data, setData] = useState<HeatmapCell[]>([]);
const [period, setPeriod] = useState<'1h'|'24h'|'7d'>('24h');
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({width: 1200, height: 700});
const layoutData = useMemo(() => {
const treemap = new SquarifiedTreemap();
return treemap.layout(data, {x: 0, y: 0, ...dimensions});
}, [data, dimensions]);
return (
<div>
<div className="flex gap-2 mb-4">
{(['1h', '24h', '7d'] as const).map(p => (
<button key={p} onClick={() => setPeriod(p)}
className={`px-3 py-1 rounded ${period === p ? 'bg-blue-600' : 'bg-gray-700'}`}>
{p}
</button>
))}
</div>
<div ref={containerRef} className="relative bg-gray-900"
style={{width: dimensions.width, height: dimensions.height}}>
{layoutData.map(cell => (
<HeatmapCell key={cell.symbol} cell={cell} period={period} />
))}
</div>
</div>
);
};
Теплова карта — це killer feature для крипто-платформи. Користувачі повертаються до неї щодня для моніторингу ринку. Хорошо реалізована система включає: фільтрацію по секторах (DeFi, Layer1, GameFi), zoom на окремі сектори та clickthrough до торгівельної пари.







