אתה מבלה שעות בניתוח טבלאות עם אלפי שורות, בניסיון למצוא דפוסים. מפת חום דוחסת את המידע הזה למסך אחד: אזורים אדומים הם שיאי פעילות, אזורים ירוקים הם פעילות נמוכה. בשתי שניות, אתה רואה שההזמנות קורות לרוב בערבי שישי. אבל בחירת הסטACK הנכון היא מסובכת: SVG מפגר על 10,000+ נקודות, Canvas דורש בדיקת היטים ידנית, ואינטגרציית backend יכולה להימשך זמן רב. אנחנו פותרים את זה מאפס: מהכנת נתונים ועד פריסה בייצור. המהנדסים שלנו משתמשים בסטACK מודרני: React, D3.js, Leaflet, Canvas — ומשלבים את הפתרונות האלה עם מערכת ה-backend שלך. התוצאה: אתה רואה דפוסים שבטבלאות דורשים דקות של ניתוח.
אילו בעיות אנחנו פותרים?
מפת חום היא כלי רב-תכליתי, אבל חשוב לבחור את סוג הוויזואליזציה הנכון. בואו נבחן ארבעה וריאנטים מרכזיים.
פעילות מבוססת זמן
שורות — ימי השבוע, עמודות — שעות. קלאסי להצגת מתי אירועים קורים (הזמנות, ביקורים, תקלות). גרף התרומות של GitHub הוא בדיוק סוג זה. זה עוזר לזהות שעות עומס ולתכנן משאבים. דוגמה: לחנות איקומרס, בנינו מפת חום של פעילות לפי יום ושעה, שגילתה שיא בהזמנות בשעה 20:00 בימי שישי. לאחר השקת התראות push ממוקדות, ההמרה עלתה ב-15%.
מטריצת מתאם
תאים N×N, ערך מ-1 עד 1. משמש בפיננסים ו-ML לניתוח תלות בין משתנים. לדוגמה, הקשר בין הוצאות פרסום להמרה.
מפת חום גיאוגרפית
שכבת צפיפות נקודות על מפה. מיושם דרך Leaflet + leaflet.heat או Mapbox. שימושי ללוגיסטיקה, קמעונאות, ניתוח תנועת רגליים.
שימור קוהורט
שורות — קוהורטות (חודש הרשמה), עמודות — תקופות שימור. אחד הכלים המרכזיים באנליטיקת מוצר. זה מראה כמה זמן משתמשים נשארים פעילים לאחר ההרשמה. אנחנו מתקנים את טווח סולם הצבעים מ-0 עד 100% כדי למנוע עיוות.
סיכום סוגי מפות חום:
| סוג | נתונים | דוגמת שימוש |
|---|---|---|
| פעילות מבוססת זמן | (יום, שעה, כמות) | גרף תרומות GitHub |
| מטריצת מתאם | (משתנה, משתנה, מקדם) | מתאמים פיננסיים |
| גיאוגרפי | (קו רוחב, קו אורך, משקל) | התפלגות לקוחות לפי מיקום |
| שימור קוהורט | (קוהורט, תקופה, %) | שימור משתמשים לאחר הרשמה |
איך לבחור בין SVG ל-Canvas?
ביצועים הם הקריטריון המרכזי. תיעוד D3.js ממליץ על Canvas לסטים גדולים של נתונים. מפת חום SVG עם 10,000+ תאים מפגרת: כל אלמנט הוא צומת DOM נפרד. Canvas מרנדר מהר יותר אבל דורש בדיקת היטים ידנית לטולטיפים. אנחנו בוחרים את הטכנולוגיה לפי המשימה.
| תכונה | SVG | Canvas |
|---|---|---|
| מספר תאים | עד 3,000 | 10,000+ |
| טולטיפ | מובנה | בדיקת היטים ידנית |
| קנה מידה | ללא אובדן | עם אובדן |
| מהירות פיתוח | מהירה יותר | מורכבת יותר |
לנפחים גדולים, אנחנו משתמשים ב-Canvas. דוגמת בדיקת היטים:
useEffect(() => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext('2d')!;
const dpr = window.devicePixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
const cellW = iw / cols.length;
const cellH = ih / rows.length;
data.forEach(d => {
const x = margin.left + cols.indexOf(d.col) * cellW;
const y = margin.top + rows.indexOf(d.row) * cellH;
ctx.fillStyle = colorScale(d.value);
ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2);
});
}, [data]);
יישום עם D3.js
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
interface HeatmapCell {
row: string;
col: string;
value: number;
}
interface HeatmapProps {
data: HeatmapCell[];
rows: string[];
cols: string[];
colorScheme?: 'blues' | 'reds' | 'greens' | 'rdylgn';
width?: number;
height?: number;
}
export function Heatmap({
data,
rows,
cols,
colorScheme = 'blues',
width = 700,
height = 400,
}: HeatmapProps) {
const svgRef = useRef<SVGSVGElement>(null);
const margin = { top: 30, right: 20, bottom: 60, left: 80 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;
useEffect(() => {
if (!svgRef.current) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleBand().domain(cols).range([0, iw]).padding(0.05);
const yScale = d3.scaleBand().domain(rows).range([0, ih]).padding(0.05);
const colorInterpolators = {
blues: d3.interpolateBlues,
reds: d3.interpolateReds,
greens: d3.interpolateGreens,
rdylgn: d3.interpolateRdYlGn,
};
const extent = d3.extent(data, d => d.value) as [number, number];
const colorScale = d3.scaleSequential()
.domain(extent)
.interpolator(colorInterpolators[colorScheme]);
// Axes
g.append('g')
.attr('transform', `translate(0,${ih})`)
.call(d3.axisBottom(xScale).tickSize(0))
.select('.domain').remove();
g.append('g')
.call(d3.axisLeft(yScale).tickSize(0))
.select('.domain').remove();
// Tooltip
const tooltip = d3.select('body').append('div')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(0,0,0,0.8)')
.style('color', '#fff')
.style('padding', '6px 10px')
.style('border-radius', '4px')
.style('font-size', '12px')
.style('pointer-events', 'none');
// Cells
g.selectAll('.cell')
.data(data)
.join('rect')
.attr('class', 'cell')
.attr('x', d => xScale(d.col)!)
.attr('y', d => yScale(d.row)!)
.attr('width', xScale.bandwidth())
.attr('height', yScale.bandwidth())
.attr('fill', d => d.value == null ? '#f0f0f0' : colorScale(d.value))
.attr('rx', 2)
.on('mouseover', (event, d) => {
tooltip
.style('display', 'block')
.style('left', `${event.pageX + 12}px`)
.style('top', `${event.pageY - 28}px`)
.html(`<strong>${d.row} / ${d.col}</strong><br/>${d3.format(',.2f')(d.value)}`);
})
.on('mouseout', () => tooltip.style('display', 'none'));
return () => {
tooltip.remove();
};
}, [data, rows, cols, colorScheme]);
return <svg ref={svgRef} width={width} height={height} />;
} מקרא גרדיאנט
function addColorLegend(
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
colorScale: d3.ScaleSequential<string>,
x: number,
y: number,
width = 200,
height = 12
) {
const defs = svg.append('defs');
const gradientId = `legend-gradient-${Math.random().toString(36).slice(2)}`;
const gradient = defs.append('linearGradient').attr('id', gradientId);
gradient.append('stop').attr('offset', '0%').attr('stop-color', colorScale(colorScale.domain()[0]));
gradient.append('stop').attr('offset', '100%').attr('stop-color', colorScale(colorScale.domain()[1]));
const legendG = svg.append('g').attr('transform', `translate(${x},${y})`);
legendG.append('rect')
.attr('width', width)
.attr('height', height)
.style('fill', `url(#${gradientId})`);
const legendScale = d3.scaleLinear()
.domain(colorScale.domain())
.range([0, width]);
legendG.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(legendScale).ticks(4).tickFormat(d3.format(',.0f')));
} מפת חום שימור קוהורט
מקרה מיוחד עם לוגיקה ייחודית — ערכים לאורך האלכסון מ-0% עד 100%:
interface CohortRow {
cohort: string; // "Jan 2024"
periods: (number | null)[]; // retention % по периодам
}
function prepareCohortData(cohorts: CohortRow[]): HeatmapCell[] {
return cohorts
.flatMap((row, rowIdx) =>
row.periods.map((value, colIdx) => ({
row: row.cohort,
col: `Period ${colIdx}`,
value: value ?? 0,
isEmpty: value === null,
}))
)
.filter(d => !d.isEmpty);
}לשימור, עדיף להשתמש ב-useEffect(() => { const canvas = canvasRef.current!; const ctx = canvas.getContext('2d')!; const dpr = window.devicePixelRatio; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; ctx.scale(dpr, dpr); const cellW = iw / cols.length; const cellH = ih / rows.length; data.forEach(d => { const x = margin.left + cols.indexOf(d.col) * cellW; const y = margin.top + rows.indexOf(d.row) * cellH; ctx.fillStyle = colorScale(d.value); ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2); }); }, [data]); — אדום לערכים נמוכים, ירוק לגבוהים. תקן את הטווח ל-[0, 100], לא מ-min ל-max, אחרת הוויזואליזציה מטעה.
למה טווח קבוע חשוב
אם הטווח דינמי, הבדל בין 80% ל-90% יוצג בצורה חיה, למרות ששני המספרים גבוהים. טווח קבוע [0,100] נותן תמונה אובייקטיבית של שימור.
איך לשלב את מפת החום עם ה-API שלך?
לטווחי זמן גדולים, נתונים מצטברים בשרת. אנחנו משתמשים ב-PostgreSQL ו-Redis לקאשינג, ומפחיתים עומס על ה-API. לדוגמה, לפעילות לפי יום בשבוע ושעה, אנחנו משתמשים בשאילתת SQL:
SELECT EXTRACT(DOW FROM created_at)::int AS dow,
EXTRACT(HOUR FROM created_at)::int AS hour,
COUNT(*) AS events
FROM user_events
WHERE created_at > NOW() - INTERVAL '90 days'
AND user_id = $1
GROUP BY 1, 2
ORDER BY 1, 2;התוצאה מועברת דרך API, עם תאים ריקים שממולאים באפסים בשרת. אנחנו משתלבים עם כל backend: Laravel, Node.js, Python. אם צריך, אנחנו מגדירים עדכונים בזמן אמת דרך WebSocket.
מה כלול?
- תיעוד API וסקירת ארכיטקטורה
- קוד מקור עם הערות
- התאמה ל-CMS שלך (WordPress, Drupal, Strapi)
- הוראות תחזוקה
- אחריות לחודש על פגמים נסתרים
לוחות זמנים
מפת חום בסיסית עם טולטיפ ומקרא — 1–2 ימים. שימור קוהורט עם הכנת נתונים נכונה — 3–5 ימים. מפת חום גיאוגרפית — הערכה נפרדת. התמחור הוא אישי.
לחברה שלנו יש מעל 5 שנים בשוק וסיפקנו 30+ פרויקטים של מפות חום. ניסיון עם Core Web Vitals מבטיח שמפת החום שלך לא תאט את האתר. בקש ייעוץ — המהנדסים שלנו ינתחו את הנתונים שלך ויציעו את הפתרון האופטימלי. צור קשר להערכה מותאמת.







