פיתוח מפת חום מותאמת אישית להדמיית נתונים (D3.js, Canvas, Leaflet)

אתם מבזבזים שעות על ניתוח טבלאות בניסיון למצוא דפוסים בהתנהגות המשתמשים. אנחנו מפתחים מפות חום (Heatmaps) לדאטה ויזואליזציה שדוחסות את המידע למסך אחד ברור. הצוות שלנו מספק את הפרויקט במפתח מלא—מהכנת הנתונים ועד הפרסום—תוך הבטחת פעילות אמינה ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
פיתוח מפת חום מותאמת אישית להדמיית נתונים (D3.js, Canvas, Leaflet)
בינוני
מ- 1 יום עד 3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

אתה מבלה שעות בניתוח טבלאות עם אלפי שורות, בניסיון למצוא דפוסים. מפת חום דוחסת את המידע הזה למסך אחד: אזורים אדומים הם שיאי פעילות, אזורים ירוקים הם פעילות נמוכה. בשתי שניות, אתה רואה שההזמנות קורות לרוב בערבי שישי. אבל בחירת הסט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 מבטיח שמפת החום שלך לא תאט את האתר. בקש ייעוץ — המהנדסים שלנו ינתחו את הנתונים שלך ויציעו את הפתרון האופטימלי. צור קשר להערכה מותאמת.