HeatmapBuilder

A Ruby gem that generates embeddable SVG heatmap visualizations with GitHub-style calendar layouts. Perfect for Rails applications and any project that needs to display activity data in a visual format.

GitHub-style Calendar

Features

  • GitHub-style calendar layouts for date-based data.
  • Vector-based output (SVG) for crisp rendering at any resolution.
  • Optional numeric values displayed in each cell.
  • Use pre-calculated scores or raw numeric values - automatic mapping to color scales.
  • Custom value-to-score conversion functions for advanced scoring logic.
  • Parametric everything: customize cell size, spacing, colors, fonts, etc.
  • Rounded corners (and circular cells, if you're into that kind of thing).
  • Dynamic palette generation from two colors or manually-specified colors.
  • OKLCH color interpolation for clean color transitions and perceptual uniformity.
  • Tooltip support with native browser fallback and JS library integration hooks.
  • Zero dependencies.

Installation

Add this line to your application's Gemfile:

gem 'heatmap-builder'

And then execute:

$ bundle

Or install it yourself as:

$ gem install heatmap-builder

Usage

Calendar Heatmaps

# GitHub-style calendar heatmap
scores_by_date = {
  '2026-01-01' => 2,
  '2026-01-02' => 4,
  '2026-01-03' => 1,
  # ... more dates
}

svg = HeatmapBuilder.build_calendar(scores: scores_by_date)

GitHub-style Calendar

Calendar Heatmap Options

You must provide either scores: or values: (but not both). All other options are optional keyword arguments with sensible defaults.

Data options:

  • scores - Hash of pre-calculated scores by date (integers from 0 to number of colors minus 1). Keys can be Date objects or date strings (e.g., '2024-01-01'). Required if values is not provided.
  • values - Hash of arbitrary numeric values by date to be automatically mapped to scores. Keys can be Date objects or date strings. Required if scores is not provided. See Using Raw Values Instead of Scores.

Value-to-score conversion options (only used with values:):

  • value_min - Minimum boundary for value-to-score mapping. Defaults to the minimum value in your data.
  • value_max - Maximum boundary for value-to-score mapping. Defaults to the maximum value in your data.
  • value_to_score - Custom callable for value-to-score conversion. Receives value:, date:, min:, max:, max_score: parameters and must return an integer between 0 and max_score. See Custom Scoring Logic for details.

Appearance options:

  • cell_size - Size of each square in pixels. Defaults to 12.
  • cell_spacing - Space between squares in pixels. Defaults to 1.
  • font_size - Font size for labels in pixels. Defaults to 8.
  • border_width - Border width around each cell in pixels. Defaults to 1.
  • border_lightness_factor - Controls the border color, derived from each cell's color by scaling its lightness (in OKLCH) by this factor. Values below 1 produce a darker border; a value of 1 makes the border match the cell color, in which case the border is omitted entirely. Must be positive. Defaults to 0.9.
  • corner_radius - Corner radius for rounded cells. Must be between 0 (square corners) and floor(cell_size/2) (circular cells). Values outside this range are automatically clamped. Defaults to 0.

Color options:

  • colors - Color palette for the heatmap. Can be a predefined palette constant (e.g., HeatmapBuilder::Calendar::GITHUB_GREEN), an array of hex color strings (e.g., %w[#ebedf0 #9be9a8 #40c463]), or a hash for OKLCH interpolation (e.g., { from: "#ebedf0", to: "#216e39", steps: 5 }). Defaults to HeatmapBuilder::Calendar::GITHUB_GREEN. See Predefined Color Palettes and Dynamic Palettes Generation.

Calendar-specific options:

  • start_of_week - First day of the week. One of :sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday. Defaults to :monday.
  • month_spacing - Extra horizontal space between months in pixels. Defaults to 0.
  • show_month_labels - Show month names at the top of the calendar. Defaults to true.
  • show_day_labels - Show day abbreviations on the left side of the calendar. Defaults to true.
  • show_outside_cells - Show cells outside the date range with inactive styling. Defaults to false.

Tooltip options:

  • tooltip - Callable invoked per active cell with date:, score:, and value: keyword arguments. Return value is used as the tooltip text. Emits a native SVG <title> element (browser fallback) and a data attribute (JS library hook). Defaults to nil (no tooltip markup).
  • tooltip_attribute - Name of the data-* attribute written on each cell's <g> wrapper for JS tooltip library pickup. Defaults to "data-tooltip". Set to nil to suppress the data attribute and use only the native <title> fallback. See Tooltips.

Internationalization options:

  • day_labels - Array of day abbreviations starting from Sunday (7 elements). Defaults to %w[S M T W T F S]. See I18n.
  • month_labels - Array of month abbreviations from January to December (12 elements). Defaults to %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]. See I18n.

Using Raw Values Instead of Scores

A score is an integer (0 to N-1) that maps directly to a color in your palette. For example, with 5 colors, valid scores are 0-4.

Instead of pre-calculating scores, you can provide raw numeric values (like 45.2, 78, 1000) and let the builder automatically map them to scores using linear distribution:

# Calendar heatmap with automatic score calculation
values_by_date = {
  Date.new(2026, 1, 1) => 45.2,
  Date.new(2026, 1, 2) => 78.5,
  Date.new(2026, 1, 3) => 12.0
}

svg = HeatmapBuilder.build_calendar(
  values: values_by_date,
  value_min: 0,
  value_max: 100
)

The builder will automatically:

  • Calculate min/max boundaries from your data if not specified
  • Map values to color scores using linear distribution
  • Clamp values outside the boundaries
  • Handle nil values by treating them as the minimum boundary

Custom Scoring Logic

By default, values are mapped to scores using linear distribution. You can provide a custom value-to-score conversion function for different behaviors like logarithmic scales, exponential curves, or custom thresholds.

The callable receives these parameters:

  • value: - The current value being converted
  • date: - The date for the data point
  • min: - The minimum boundary value
  • max: - The maximum boundary value
  • max_score: - The maximum valid score (color palette length minus 1)

The function must return an integer between 0 and max_score.

Custom scoring logic - logarithmic scale for data with wide range (e.g., 1 to 10000):

logarithmic_formula = ->(value:, date:, min:, max:, max_score:) {
  return 0 if value <= 0 || min <= 0

  log_value = Math.log10(value)
  log_min = Math.log10(min)
  log_max = Math.log10(max)

  ((log_value - log_min) / (log_max - log_min) * max_score).round.clamp(0, max_score)
}

svg = HeatmapBuilder.build_calendar(
  values: values_by_date,
  value_to_score: logarithmic_formula
)

Predefined Color Palettes

GitHub Green (Default)

HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::GITHUB_GREEN)

Default Calendar

Blue Ocean

HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::BLUE_OCEAN)

Blue Ocean Calendar

Warm Sunset

HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::WARM_SUNSET)

Warm Sunset Calendar

Purple Vibes

HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::PURPLE_VIBES)

Purple Vibes Calendar

Red to Green

HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::RED_TO_GREEN)

Red to Green Calendar

Dynamic Palettes Generation

Generate custom color palettes from any two colors using OKLCH color space for superior color interpolation:

# Generate a 5-step palette from electric cyan to hot magenta
neon_gradient = {
  from: "#00FFFF",
  to: "#FF1493",
  steps: 5
}

svg = HeatmapBuilder.build_calendar(scores: calendar_data, colors: neon_gradient)

The OKLCH color space ensures perceptually uniform color transitions, making gradients appear smooth and natural to the human eye.

Cell Borders

Each cell has a border whose color is derived from the cell's own color. The border_width option sets its thickness, and border_lightness_factor controls its shade: the cell color's OKLCH lightness is multiplied by this factor, so values below 1 produce a darker border (the default 0.9 is a subtle darkening).

Setting border_lightness_factor to 1 keeps the border color identical to the cell color. In that case the border is invisible, so it is omitted from the SVG entirely.

HeatmapBuilder.build_calendar(
  scores: calendar_data,
  border_width: 1,
  border_lightness_factor: 0.7
)

Calendar with Cell Borders

With border_lightness_factor set to 1, the border is omitted entirely, leaving cells edge to edge:

HeatmapBuilder.build_calendar(
  scores: calendar_data,
  border_width: 1,
  border_lightness_factor: 1
)

Calendar with No Cell Borders

Rounded Corners

Calendar heatmaps support rounded corners using the corner_radius option.

The corner_radius value must be between 0 (square corners) and floor(cell_size/2). Values outside this range are automatically clamped to the valid range (negative values become 0, values exceeding the maximum become floor(cell_size/2)).

A typical value is around 2 pixels for a subtle rounded effect:

# Calendar heatmap with rounded corners
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  corner_radius: 2,
  cell_size: 14
)

Calendar Rounded Corners

Maximum radius values render circular cells:

# Calendar heatmap with max radius rounded corners - circular cells
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  corner_radius: 7,
  cell_size: 14
)

Calendar Rounded Corners

Month Spacing

Add visual separation between months using the month_spacing option. This adds horizontal gaps at month boundaries, making it easier to distinguish individual months:

HeatmapBuilder.build_calendar(
  scores: calendar_data,
  cell_size: 14,
  month_spacing: 10,
  corner_radius: 3
)

Calendar with Month Spacing

Tooltips

The tooltip: option accepts a callable that is invoked once per active cell. It receives date:, score:, and value: keyword arguments and should return the tooltip text string. value: is the original input value when values: mode is used, and nil when scores: mode is used.

HeatmapBuilder.build_calendar(
  scores: calendar_data,
  tooltip: ->(date:, score:, value: nil) { "#{date.strftime('%b %-d')}: #{score} contributions" }
)

When tooltip: is set, each active cell is wrapped in an SVG <g> element containing a <title> child. The <title> element is the standard SVG mechanism for native browser tooltips — it works out of the box in any browser that renders inline SVG, with no JavaScript required.

<g data-tooltip="Jan 15: 4 contributions">
  <title>Jan 15: 4 contributions</title>
  <rect fill="#40c463" .../>
</g>

The tooltip_attribute: option (default: "data-tooltip") controls which data-* attribute is emitted on the <g> wrapper. This attribute is the hook for any JS tooltip library:

// Tippy.js — one line of initialization
tippy('[data-tooltip]', { content: el => el.dataset.tooltip })

Set tooltip_attribute: nil to suppress the data attribute and rely solely on the native <title> fallback. Use any other attribute name to match your tooltip library's expected selector:

# Matches data-tippy-content used by some Tippy.js configurations
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  tooltip: ->(date:, score:, value: nil) { "#{date}: #{score}" },
  tooltip_attribute: "data-tippy-content"
)

Outside cells rendered via show_outside_cells: true never receive tooltip markup.

I18n

Calendar heatmaps support internationalization by customizing the day_labels and month_labels options:

# French calendar
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  day_labels: %w[D L M M J V S],  # Dimanche, Lundi, Mardi, etc.
  month_labels: %w[Jan Fév Mar Avr Mai Jun Jul Aoû Sep Oct Nov Déc]
)

# German calendar
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  day_labels: %w[S M D M D F S],  # Sonntag, Montag, Dienstag, etc.
  month_labels: %w[Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez]
)

# Italian calendar
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  day_labels: %w[D L M M G V S],  # Domenica, Lunedì, Martedì, etc.
  month_labels: %w[Gen Feb Mar Apr Mag Giu Lug Ago Set Ott Nov Dic]
)

# Spanish calendar
HeatmapBuilder.build_calendar(
  scores: calendar_data,
  day_labels: %w[D L M X J V S],  # Domingo, Lunes, Martes, etc.
  month_labels: %w[Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic]
)

The day_labels array should contain 7 elements starting from Sunday, and month_labels should contain 12 elements for January through December.

Development

After checking out the repo, run bin/setup to install development dependencies.

Running Tests

# Run all tests
rake test

# Run tests with code linting
rake

# Update test snapshots after making intentional changes to output
rake update_snapshots

To generate all example SVG files you see in this readme:

bin/generate_examples

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/dreikanter/heatmap-builder. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the HeatmapBuilder project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.