Heatmaps are powerful data visualization tools that represent data values using colors on a two-dimensional surface. They're widely used in various domains like analytics, geography, biology, and user experience research. In this article, we'll build a complete heatmap generation system in Java.
Project Setup
First, add the necessary dependencies to your pom.xml:
<dependencies> <!-- For image processing and output --> <dependency> <groupId>com.twelvemonkeys.imageio</groupId> <artifactId>imageio</artifactId> <version>3.9.4</version> </dependency> <dependency> <groupId>com.twelvemonkeys.imageio</groupId> <artifactId>imageio-jpeg</artifactId> <version>3.9.4</version> </dependency> <!-- For mathematical operations --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-math3</artifactId> <version>3.6.1</version> </dependency> <!-- For JSON processing (if needed) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- For logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Core Implementation
1. Data Models and Configuration
DataPoint.java - Represents a single data point with coordinates and intensity
public class DataPoint {
private final double x;
private final double y;
private final double intensity;
public DataPoint(double x, double y, double intensity) {
this.x = x;
this.y = y;
this.intensity = intensity;
}
public DataPoint(double x, double y) {
this(x, y, 1.0);
}
// Getters
public double getX() { return x; }
public double getY() { return y; }
public double getIntensity() { return intensity; }
@Override
public String toString() {
return String.format("(%.2f, %.2f): %.2f", x, y, intensity);
}
}
HeatmapConfig.java - Configuration for heatmap generation
public class HeatmapConfig {
private final int width;
private final int height;
private final double radius;
private final ColorGradient colorGradient;
private final double blurRadius;
private final boolean normalize;
private final double opacity;
private HeatmapConfig(Builder builder) {
this.width = builder.width;
this.height = builder.height;
this.radius = builder.radius;
this.colorGradient = builder.colorGradient;
this.blurRadius = builder.blurRadius;
this.normalize = builder.normalize;
this.opacity = builder.opacity;
}
public static class Builder {
private int width = 800;
private int height = 600;
private double radius = 50.0;
private ColorGradient colorGradient = ColorGradient.DEFAULT;
private double blurRadius = 1.5;
private boolean normalize = true;
private double opacity = 0.8;
public Builder width(int width) {
this.width = width;
return this;
}
public Builder height(int height) {
this.height = height;
return this;
}
public Builder radius(double radius) {
this.radius = radius;
return this;
}
public Builder colorGradient(ColorGradient gradient) {
this.colorGradient = gradient;
return this;
}
public Builder blurRadius(double blurRadius) {
this.blurRadius = blurRadius;
return this;
}
public Builder normalize(boolean normalize) {
this.normalize = normalize;
return this;
}
public Builder opacity(double opacity) {
this.opacity = Math.max(0.0, Math.min(1.0, opacity));
return this;
}
public HeatmapConfig build() {
return new HeatmapConfig(this);
}
}
// Getters
public int getWidth() { return width; }
public int getHeight() { return height; }
public double getRadius() { return radius; }
public ColorGradient getColorGradient() { return colorGradient; }
public double getBlurRadius() { return blurRadius; }
public boolean isNormalize() { return normalize; }
public double getOpacity() { return opacity; }
}
2. Color Gradients
ColorGradient.java - Defines color transitions for the heatmap
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
public class ColorGradient {
private final List<ColorStop> colorStops;
// Predefined gradients
public static final ColorGradient DEFAULT = new Builder()
.addStop(0.0, new Color(0, 0, 255)) // Blue
.addStop(0.5, new Color(0, 255, 0)) // Green
.addStop(1.0, new Color(255, 0, 0)) // Red
.build();
public static final ColorGradient FIRE = new Builder()
.addStop(0.0, Color.BLACK)
.addStop(0.3, Color.RED)
.addStop(0.6, Color.YELLOW)
.addStop(1.0, Color.WHITE)
.build();
public static final ColorGradient COLD_HOT = new Builder()
.addStop(0.0, Color.BLUE)
.addStop(0.5, Color.CYAN)
.addStop(0.7, Color.GREEN)
.addStop(0.9, Color.YELLOW)
.addStop(1.0, Color.RED)
.build();
public static final ColorGradient GRAYSCALE = new Builder()
.addStop(0.0, Color.BLACK)
.addStop(1.0, Color.WHITE)
.build();
private ColorGradient(List<ColorStop> colorStops) {
this.colorStops = new ArrayList<>(colorStops);
this.colorStops.sort((a, b) -> Double.compare(a.position, b.position));
}
public Color getColor(double value) {
if (colorStops.isEmpty()) return Color.BLACK;
if (value <= 0.0) return colorStops.get(0).color;
if (value >= 1.0) return colorStops.get(colorStops.size() - 1).color;
for (int i = 0; i < colorStops.size() - 1; i++) {
ColorStop current = colorStops.get(i);
ColorStop next = colorStops.get(i + 1);
if (value >= current.position && value <= next.position) {
double t = (value - current.position) / (next.position - current.position);
return interpolateColor(current.color, next.color, t);
}
}
return colorStops.get(colorStops.size() - 1).color;
}
private Color interpolateColor(Color start, Color end, double t) {
int r = (int) (start.getRed() + t * (end.getRed() - start.getRed()));
int g = (int) (start.getGreen() + t * (end.getGreen() - start.getGreen()));
int b = (int) (start.getBlue() + t * (end.getBlue() - start.getBlue()));
int a = (int) (start.getAlpha() + t * (end.getAlpha() - start.getAlpha()));
return new Color(
Math.max(0, Math.min(255, r)),
Math.max(0, Math.min(255, g)),
Math.max(0, Math.min(255, b)),
Math.max(0, Math.min(255, a))
);
}
public static class Builder {
private List<ColorStop> stops = new ArrayList<>();
public Builder addStop(double position, Color color) {
stops.add(new ColorStop(position, color));
return this;
}
public ColorGradient build() {
if (stops.isEmpty()) {
throw new IllegalStateException("At least one color stop is required");
}
return new ColorGradient(stops);
}
}
private static class ColorStop {
final double position;
final Color color;
ColorStop(double position, Color color) {
this.position = position;
this.color = color;
}
}
}
3. Core Heatmap Generator
HeatmapGenerator.java - Main class for generating heatmaps
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class HeatmapGenerator {
private final HeatmapConfig config;
public HeatmapGenerator(HeatmapConfig config) {
this.config = config;
}
public BufferedImage generateHeatmap(List<DataPoint> dataPoints) {
// Step 1: Create intensity matrix
double[][] intensityMatrix = createIntensityMatrix(dataPoints);
// Step 2: Apply Gaussian blur for smoothing
if (config.getBlurRadius() > 0) {
intensityMatrix = applyGaussianBlur(intensityMatrix, config.getBlurRadius());
}
// Step 3: Normalize intensities if requested
if (config.isNormalize()) {
intensityMatrix = normalizeMatrix(intensityMatrix);
}
// Step 4: Create colored image
return createColoredImage(intensityMatrix);
}
private double[][] createIntensityMatrix(List<DataPoint> dataPoints) {
double[][] matrix = new double[config.getHeight()][config.getWidth()];
double radiusSquared = config.getRadius() * config.getRadius();
// Use parallel processing for large datasets
List<DataPoint> threadSafeData = new CopyOnWriteArrayList<>(dataPoints);
// Initialize matrix with zeros
for (int y = 0; y < config.getHeight(); y++) {
for (int x = 0; x < config.getWidth(); x++) {
matrix[y][x] = 0.0;
}
}
// Add influence from each data point
for (DataPoint point : threadSafeData) {
int centerX = (int) (point.getX() * config.getWidth());
int centerY = (int) (point.getY() * config.getHeight());
int startX = Math.max(0, (int) (centerX - config.getRadius()));
int endX = Math.min(config.getWidth() - 1, (int) (centerX + config.getRadius()));
int startY = Math.max(0, (int) (centerY - config.getRadius()));
int endY = Math.min(config.getHeight() - 1, (int) (centerY + config.getRadius()));
for (int y = startY; y <= endY; y++) {
for (int x = startX; x <= endX; x++) {
double dx = (x - centerX);
double dy = (y - centerY);
double distanceSquared = dx * dx + dy * dy;
if (distanceSquared <= radiusSquared) {
// Inverse square law for intensity falloff
double influence = 1.0 - (distanceSquared / radiusSquared);
influence = influence * influence; // Quadratic falloff
matrix[y][x] += point.getIntensity() * influence;
}
}
}
}
return matrix;
}
private double[][] applyGaussianBlur(double[][] matrix, double sigma) {
int size = (int) (sigma * 3) * 2 + 1; // Kernel size
if (size < 3) return matrix;
double[][] kernel = createGaussianKernel(size, sigma);
return convolve(matrix, kernel);
}
private double[][] createGaussianKernel(int size, double sigma) {
double[][] kernel = new double[size][size];
double sum = 0.0;
int center = size / 2;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
double dx = x - center;
double dy = y - center;
double value = Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma));
kernel[y][x] = value;
sum += value;
}
}
// Normalize kernel
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
kernel[y][x] /= sum;
}
}
return kernel;
}
private double[][] convolve(double[][] matrix, double[][] kernel) {
int height = matrix.length;
int width = matrix[0].length;
int kernelSize = kernel.length;
int kernelRadius = kernelSize / 2;
double[][] result = new double[height][width];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double sum = 0.0;
for (int ky = 0; ky < kernelSize; ky++) {
for (int kx = 0; kx < kernelSize; kx++) {
int px = x + kx - kernelRadius;
int py = y + ky - kernelRadius;
if (px >= 0 && px < width && py >= 0 && py < height) {
sum += matrix[py][px] * kernel[ky][kx];
}
}
}
result[y][x] = sum;
}
}
return result;
}
private double[][] normalizeMatrix(double[][] matrix) {
int height = matrix.length;
int width = matrix[0].length;
// Find min and max values
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (matrix[y][x] < min) min = matrix[y][x];
if (matrix[y][x] > max) max = matrix[y][x];
}
}
// Normalize to [0, 1] range
double[][] normalized = new double[height][width];
double range = max - min;
if (range > 0) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
normalized[y][x] = (matrix[y][x] - min) / range;
}
}
}
return normalized;
}
private BufferedImage createColoredImage(double[][] intensityMatrix) {
int height = intensityMatrix.length;
int width = intensityMatrix[0].length;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double intensity = intensityMatrix[y][x];
Color color = config.getColorGradient().getColor(intensity);
// Apply opacity
if (config.getOpacity() < 1.0) {
int alpha = (int) (color.getAlpha() * config.getOpacity());
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
}
image.setRGB(x, y, color.getRGB());
}
}
return image;
}
}
4. Image Utilities
ImageUtils.java - Helper methods for image operations
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class ImageUtils {
public static void saveImage(BufferedImage image, String filename, String format) throws IOException {
File output = new File(filename);
ImageIO.write(image, format, output);
System.out.println("Heatmap saved to: " + output.getAbsolutePath());
}
public static BufferedImage createComposite(BufferedImage heatmap, BufferedImage background) {
int width = heatmap.getWidth();
int height = heatmap.getHeight();
// Scale background to match heatmap size if needed
if (background.getWidth() != width || background.getHeight() != height) {
background = scaleImage(background, width, height);
}
BufferedImage composite = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = composite.createGraphics();
// Draw background
g2d.drawImage(background, 0, 0, null);
// Draw heatmap with transparency
g2d.drawImage(heatmap, 0, 0, null);
g2d.dispose();
return composite;
}
public static BufferedImage scaleImage(BufferedImage original, int newWidth, int newHeight) {
BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = scaled.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(original, 0, 0, newWidth, newHeight, null);
g2d.dispose();
return scaled;
}
public static BufferedImage createGridOverlay(int width, int height, int gridSize, Color gridColor) {
BufferedImage grid = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = grid.createGraphics();
g2d.setColor(gridColor);
g2d.setStroke(new BasicStroke(1));
// Draw vertical lines
for (int x = 0; x < width; x += gridSize) {
g2d.drawLine(x, 0, x, height);
}
// Draw horizontal lines
for (int y = 0; y < height; y += gridSize) {
g2d.drawLine(0, y, width, y);
}
g2d.dispose();
return grid;
}
}
5. Data Generation Utilities
DataGenerator.java - Generate sample data for testing
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class DataGenerator {
public static List<DataPoint> generateRandomPoints(int count, double maxX, double maxY) {
List<DataPoint> points = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < count; i++) {
double x = random.nextDouble() * maxX;
double y = random.nextDouble() * maxY;
double intensity = 0.5 + random.nextDouble() * 0.5; // Intensity between 0.5 and 1.0
points.add(new DataPoint(x, y, intensity));
}
return points;
}
public static List<DataPoint> generateClusterPoints(int clusters, int pointsPerCluster,
double clusterSpread, double maxX, double maxY) {
List<DataPoint> points = new ArrayList<>();
Random random = new Random();
for (int c = 0; c < clusters; c++) {
double centerX = random.nextDouble() * maxX;
double centerY = random.nextDouble() * maxY;
for (int i = 0; i < pointsPerCluster; i++) {
double x = centerX + (random.nextGaussian() * clusterSpread);
double y = centerY + (random.nextGaussian() * clusterSpread);
double intensity = 0.7 + random.nextDouble() * 0.3;
// Ensure points stay within bounds
x = Math.max(0, Math.min(maxX, x));
y = Math.max(0, Math.min(maxY, y));
points.add(new DataPoint(x, y, intensity));
}
}
return points;
}
public static List<DataPoint> generateLinearDistribution(int count, double slope,
double intercept, double noise,
double maxX, double maxY) {
List<DataPoint> points = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < count; i++) {
double x = random.nextDouble() * maxX;
double y = slope * x + intercept + (random.nextGaussian() * noise);
double intensity = 0.5 + random.nextDouble() * 0.5;
if (y >= 0 && y <= maxY) {
points.add(new DataPoint(x, y, intensity));
}
}
return points;
}
}
6. Demonstration Class
HeatmapDemo.java - Showcase different heatmap configurations
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class HeatmapDemo {
public static void main(String[] args) {
try {
// Example 1: Basic random points with default gradient
basicRandomHeatmap();
// Example 2: Clustered data with fire gradient
clusteredHeatmap();
// Example 3: Linear distribution with custom gradient
linearDistributionHeatmap();
// Example 4: High-resolution heatmap with transparency
highResolutionHeatmap();
} catch (Exception e) {
System.err.println("Error generating heatmaps: " + e.getMessage());
e.printStackTrace();
}
}
private static void basicRandomHeatmap() throws IOException {
System.out.println("=== Generating Basic Random Heatmap ===");
// Generate random data points
List<DataPoint> dataPoints = DataGenerator.generateRandomPoints(1000, 1.0, 1.0);
// Configure heatmap
HeatmapConfig config = new HeatmapConfig.Builder()
.width(800)
.height(600)
.radius(0.05)
.colorGradient(ColorGradient.DEFAULT)
.blurRadius(2.0)
.opacity(0.9)
.build();
// Generate heatmap
HeatmapGenerator generator = new HeatmapGenerator(config);
BufferedImage heatmap = generator.generateHeatmap(dataPoints);
// Save result
ImageUtils.saveImage(heatmap, "basic_random_heatmap.png", "PNG");
}
private static void clusteredHeatmap() throws IOException {
System.out.println("\n=== Generating Clustered Heatmap ===");
// Generate clustered data
List<DataPoint> dataPoints = DataGenerator.generateClusterPoints(5, 200, 0.05, 1.0, 1.0);
// Configure with fire gradient
HeatmapConfig config = new HeatmapConfig.Builder()
.width(800)
.height(600)
.radius(0.04)
.colorGradient(ColorGradient.FIRE)
.blurRadius(1.5)
.opacity(0.85)
.build();
HeatmapGenerator generator = new HeatmapGenerator(config);
BufferedImage heatmap = generator.generateHeatmap(dataPoints);
ImageUtils.saveImage(heatmap, "clustered_heatmap.png", "PNG");
}
private static void linearDistributionHeatmap() throws IOException {
System.out.println("\n=== Generating Linear Distribution Heatmap ===");
// Generate linear distribution with noise
List<DataPoint> dataPoints = DataGenerator.generateLinearDistribution(800, 0.7, 0.1, 0.05, 1.0, 1.0);
// Create custom gradient
ColorGradient customGradient = new ColorGradient.Builder()
.addStop(0.0, new Color(0, 0, 128)) // Dark blue
.addStop(0.3, new Color(0, 128, 255)) // Light blue
.addStop(0.6, new Color(0, 255, 255)) // Cyan
.addStop(0.8, new Color(255, 255, 0)) // Yellow
.addStop(1.0, new Color(255, 0, 0)) // Red
.build();
HeatmapConfig config = new HeatmapConfig.Builder()
.width(800)
.height(600)
.radius(0.03)
.colorGradient(customGradient)
.blurRadius(1.2)
.normalize(true)
.opacity(0.9)
.build();
HeatmapGenerator generator = new HeatmapGenerator(config);
BufferedImage heatmap = generator.generateHeatmap(dataPoints);
ImageUtils.saveImage(heatmap, "linear_heatmap.png", "PNG");
}
private static void highResolutionHeatmap() throws IOException {
System.out.println("\n=== Generating High-Resolution Heatmap ===");
List<DataPoint> dataPoints = DataGenerator.generateClusterPoints(8, 300, 0.03, 1.0, 1.0);
HeatmapConfig config = new HeatmapConfig.Builder()
.width(1920)
.height(1080)
.radius(0.02)
.colorGradient(ColorGradient.COLD_HOT)
.blurRadius(3.0)
.opacity(0.8)
.build();
HeatmapGenerator generator = new HeatmapGenerator(config);
BufferedImage heatmap = generator.generateHeatmap(dataPoints);
// Create a background (optional)
BufferedImage background = createGradientBackground(1920, 1080, Color.BLACK, Color.DARK_GRAY);
BufferedImage composite = ImageUtils.createComposite(heatmap, background);
ImageUtils.saveImage(composite, "high_res_heatmap.png", "PNG");
}
private static BufferedImage createGradientBackground(int width, int height, Color top, Color bottom) {
BufferedImage background = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = background.createGraphics();
GradientPaint gradient = new GradientPaint(0, 0, top, 0, height, bottom);
g2d.setPaint(gradient);
g2d.fillRect(0, 0, width, height);
g2d.dispose();
return background;
}
}
Advanced Features
7. Real-time Heatmap Generation
RealTimeHeatmap.java - For dynamic data updates
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class RealTimeHeatmap {
private final HeatmapConfig config;
private final HeatmapGenerator generator;
private final List<DataPoint> dataPoints;
public RealTimeHeatmap(HeatmapConfig config) {
this.config = config;
this.generator = new HeatmapGenerator(config);
this.dataPoints = new CopyOnWriteArrayList<>();
}
public void addDataPoint(DataPoint point) {
dataPoints.add(point);
}
public void addDataPoints(List<DataPoint> points) {
dataPoints.addAll(points);
}
public void clearData() {
dataPoints.clear();
}
public BufferedImage generateCurrentHeatmap() {
return generator.generateHeatmap(dataPoints);
}
public int getDataPointCount() {
return dataPoints.size();
}
}
Performance Optimization Tips
- Use parallel processing for large datasets
- Implement level-of-detail rendering for different zoom levels
- Cache computed kernels for Gaussian blur
- Use spatial indexing (quadtrees) for large numbers of points
- Implement incremental updates for real-time applications
Expected Output
The demo will generate several PNG files:
basic_random_heatmap.png- Random point distribution with blue-green-red gradientclustered_heatmap.png- Clustered data with fire gradientlinear_heatmap.png- Linear pattern with custom gradienthigh_res_heatmap.png- High-resolution version with background
This implementation provides a complete, flexible heatmap generation system that can handle various data distributions and visualization requirements. The modular design makes it easy to extend with new color gradients, blur algorithms, or data sources.