Skip to content
Tolinku
Tolinku
Sign In Start Free
Analytics & Attribution · · 4 min read

Deep Link Heatmaps: Visualizing Click Patterns

By Tolinku Staff
|
Tolinku analytics measurement dashboard screenshot for analytics blog posts

Tables tell you the numbers. Heatmaps tell you the patterns. A geographic heatmap instantly shows that your campaign is performing well in coastal US cities but failing in the Midwest. A time-based heatmap reveals that your push notification deep links get the most engagement at 8am and 6pm. These patterns are invisible in raw data.

This guide covers heatmap visualizations for deep link analytics. For geographic analytics, see geographic analytics for deep link campaigns. For visualization techniques, see visualizing deep link data: charts and dashboards.

Tolinku analytics dashboard showing click metrics and conversion funnel The analytics dashboard with date range selector, filters, charts, and breakdowns.

Geographic Heatmap

A geographic heatmap (choropleth) colors countries, states, or cities by click volume or conversion rate:

Region Clicks Conversion Rate Color Intensity
California 8,200 14.5% Dark (high)
Texas 4,100 12.8% Medium
New York 5,500 13.2% Medium-dark
Florida 3,800 11.5% Medium
Iowa 200 8.0% Light (low)

Geographic heatmaps expose regional patterns that a national average obscures. If your overall conversion rate is 12%, but California is at 14.5% and Iowa is at 8%, you can investigate why and optimize accordingly.

Time-Based Heatmap

A time-based heatmap shows click activity by hour and day of week:

Hour/Day    Mon   Tue   Wed   Thu   Fri   Sat   Sun
──────────────────────────────────────────────────
06:00       ░░    ░░    ░░    ░░    ░░    ░     ░
07:00       ▒▒    ▒▒    ▒▒    ▒▒    ▒▒    ░     ░
08:00       ▓▓    ▓▓    ▓▓    ▓▓    ▓▓    ░░    ░
09:00       ██    ██    ██    ██    ▓▓    ░░    ░
10:00       ▓▓    ▓▓    ▓▓    ▓▓    ▓▓    ▒▒    ░░
11:00       ▓▓    ▓▓    ▓▓    ▓▓    ▓▓    ▒▒    ▒▒
12:00       ██    ██    ██    ██    ██    ▒▒    ▒▒
13:00       ▓▓    ▓▓    ▓▓    ▓▓    ▓▓    ▒▒    ▒▒
...
18:00       ██    ██    ██    ██    ▓▓    ▓▓    ▓▓
19:00       ▓▓    ▓▓    ▓▓    ▓▓    ▒▒    ▓▓    ▓▓

█ = High activity   ▓ = Medium   ▒ = Low   ░ = Very low

This pattern is common for B2C apps: weekday commute hours (8-9am, 6-7pm) and lunch (12pm) peak. Weekends shift later.

Route Heatmap

Show which deep link routes receive the most traffic:

SELECT
  route,
  source,
  COUNT(*) AS clicks,
  ROUND(SUM(CASE WHEN outcome = 'app_opened' THEN 1 ELSE 0 END)::DECIMAL / COUNT(*) * 100, 1) AS open_rate
FROM deep_link_clicks
WHERE timestamp >= NOW() - INTERVAL '7 days'
GROUP BY route, source
ORDER BY clicks DESC;
Route Email Push Social QR Total
/offers/summer 5,200 3,800 1,200 800 11,000
/products/:id 3,100 1,500 2,400 200 7,200
/referral 800 0 3,500 0 4,300
/dashboard 1,200 2,100 0 0 3,300
/settings 400 800 0 0 1,200

Color each cell by intensity. This reveals which routes are driven by which channels, and which combinations are underutilized.

Building Heatmaps

Data Preparation

Transform raw click data into a matrix for heatmap rendering:

interface HeatmapCell {
  row: string;    // e.g., hour of day
  column: string; // e.g., day of week
  value: number;  // e.g., click count
}

function buildTimeHeatmap(clicks: ClickData[]): HeatmapCell[] {
  const matrix: Record<string, Record<string, number>> = {};

  for (const click of clicks) {
    const date = new Date(click.timestamp);
    const hour = date.getUTCHours().toString().padStart(2, '0') + ':00';
    const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getUTCDay()];

    if (!matrix[hour]) matrix[hour] = {};
    matrix[hour][day] = (matrix[hour][day] || 0) + 1;
  }

  const cells: HeatmapCell[] = [];
  for (const [hour, days] of Object.entries(matrix)) {
    for (const [day, count] of Object.entries(days)) {
      cells.push({ row: hour, column: day, value: count });
    }
  }

  return cells;
}

Color Scales

Choose a color scale that communicates intensity:

Scale Best For Example
Sequential (white to blue) Single metric (click count) More clicks = darker blue
Diverging (red to blue) Deviation from average Below average = red, above = blue
Categorical Outcome type (app open, fallback, error) Green = open, yellow = fallback, red = error
function getSequentialColor(value: number, max: number): string {
  const intensity = Math.min(value / max, 1);
  const r = Math.round(255 - (intensity * 200));
  const g = Math.round(255 - (intensity * 150));
  const b = 255;
  return `rgb(${r}, ${g}, ${b})`;
}

function getDivergingColor(value: number, average: number, max: number): string {
  const deviation = (value - average) / max;
  if (deviation > 0) {
    // Above average: blue
    return `rgb(${Math.round(255 - deviation * 200)}, ${Math.round(255 - deviation * 150)}, 255)`;
  } else {
    // Below average: red
    return `rgb(255, ${Math.round(255 + deviation * 200)}, ${Math.round(255 + deviation * 200)})`;
  }
}

Rendering with D3.js

import * as d3 from 'd3';

function renderHeatmap(
  container: HTMLElement,
  data: HeatmapCell[],
  config: { width: number; height: number; cellSize: number }
) {
  const svg = d3.select(container)
    .append('svg')
    .attr('width', config.width)
    .attr('height', config.height);

  const colorScale = d3.scaleSequential(d3.interpolateBlues)
    .domain([0, d3.max(data, d => d.value) || 1]);

  const rows = [...new Set(data.map(d => d.row))].sort();
  const cols = [...new Set(data.map(d => d.column))];

  svg.selectAll('rect')
    .data(data)
    .join('rect')
    .attr('x', d => cols.indexOf(d.column) * config.cellSize)
    .attr('y', d => rows.indexOf(d.row) * config.cellSize)
    .attr('width', config.cellSize - 1)
    .attr('height', config.cellSize - 1)
    .attr('fill', d => colorScale(d.value))
    .append('title')
    .text(d => `${d.row} ${d.column}: ${d.value} clicks`);
}

Use Cases

Optimizing Send Times

Use time heatmaps to find the best hours for push notifications and emails:

SELECT
  EXTRACT(HOUR FROM timestamp) AS hour,
  EXTRACT(DOW FROM timestamp) AS day_of_week,
  COUNT(*) AS clicks,
  ROUND(SUM(CASE WHEN converted THEN 1 ELSE 0 END)::DECIMAL / COUNT(*) * 100, 1) AS conv_rate
FROM deep_link_clicks
WHERE source = 'push'
  AND timestamp >= NOW() - INTERVAL '30 days'
GROUP BY hour, day_of_week
ORDER BY conv_rate DESC;

If Tuesday at 9am has a 15% conversion rate but Friday at 3pm has 6%, schedule your high-value campaigns for Tuesday morning.

Identifying Regional Issues

If your geographic heatmap shows high clicks but low conversions in a specific region, investigate:

  • App store availability: Is the app available in that country's store?
  • Language: Is the deep link landing content in the user's language?
  • Platform distribution: Does the region skew heavily Android, where you might have App Links issues?
  • Network speed: Slow connections may cause timeouts before the deep link resolves.

Detecting Fraud

Click fraud often shows up as anomalous patterns in heatmaps:

Pattern What It Might Indicate
Single country with 10x normal click volume Click farm
Clicks concentrated in a 1-hour window Bot attack
High clicks but 0% conversion from one source Fraudulent traffic
Uniform distribution across all hours (no peaks/valleys) Automated clicking (bots do not sleep)

Tolinku for Heatmap Data

Tolinku's analytics provide the click data needed for heatmap visualizations: timestamps, geographic data, routes, and sources. Export this data from the Tolinku dashboard or query it via the analytics API for custom heatmap rendering.

For geographic analytics, see geographic analytics for deep link campaigns. For analytics fundamentals, see deep link analytics: measuring what matters.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.