heatmap-builder 0.2.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -5
  3. data/CHANGELOG.md +70 -3
  4. data/Gemfile.lock +27 -24
  5. data/README.md +105 -135
  6. data/bin/generate_examples +42 -99
  7. data/examples/calendar_blue_ocean.svg +1 -1
  8. data/examples/calendar_cell_borders.svg +1 -0
  9. data/examples/calendar_default.svg +1 -1
  10. data/examples/calendar_github_style.svg +1 -1
  11. data/examples/calendar_month_spacing_rounded.svg +1 -0
  12. data/examples/calendar_no_borders.svg +1 -0
  13. data/examples/calendar_purple_vibes.svg +1 -1
  14. data/examples/calendar_red_to_green.svg +1 -1
  15. data/examples/calendar_rounded_corners.svg +1 -1
  16. data/examples/calendar_rounded_corners_max_radius.svg +1 -1
  17. data/examples/calendar_sunday_start.svg +1 -1
  18. data/examples/calendar_warm_sunset.svg +1 -1
  19. data/examples/calendar_with_outside_cells.svg +1 -1
  20. data/heatmap-builder.gemspec +4 -4
  21. data/lib/heatmap-builder.rb +5 -31
  22. data/lib/heatmap_builder/calendar.rb +426 -0
  23. data/lib/heatmap_builder/color_helpers.rb +121 -141
  24. data/lib/heatmap_builder/svg_helpers.rb +11 -4
  25. data/lib/heatmap_builder/value_conversion.rb +9 -5
  26. data/lib/heatmap_builder/version.rb +1 -1
  27. metadata +13 -22
  28. data/examples/large_cells.svg +0 -1
  29. data/examples/linear_blue_ocean.svg +0 -1
  30. data/examples/linear_github_green.svg +0 -1
  31. data/examples/linear_neon_gradient.svg +0 -1
  32. data/examples/linear_purple_vibes.svg +0 -1
  33. data/examples/linear_red_to_green.svg +0 -1
  34. data/examples/linear_rounded_corners.svg +0 -1
  35. data/examples/linear_rounded_corners_max_radius.svg +0 -1
  36. data/examples/linear_warm_sunset.svg +0 -1
  37. data/examples/weekly_progress.svg +0 -1
  38. data/lib/heatmap_builder/builder.rb +0 -100
  39. data/lib/heatmap_builder/calendar_heatmap_builder.rb +0 -310
  40. data/lib/heatmap_builder/linear_heatmap_builder.rb +0 -74
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf666db73abc3356f19fea3869f4a6bb4af21c28a2036f2926c811250636c904
4
- data.tar.gz: 6ba3349f270af6ef2216d7501888187aa39e2c11d8b874d084e861cf3f41ef8b
3
+ metadata.gz: 6493e6ea0c3c5fd533269a55e6cb18c8df0a65ddad86aab5061c6d5cf57c5586
4
+ data.tar.gz: fd5ae27f32ec084c3414d8f26a7def6c45bedef66b7266423ddd9fc3ec3df458
5
5
  SHA512:
6
- metadata.gz: 65f06b77aeeda0e45054bbfe402b9d344bf2996a7a5423ca17ab005f79a2aaced93a656eb830294d021b46c2c7ad3c01338553d7d8ece9259d7208ace6ece1b9
7
- data.tar.gz: 5fdb4ea1d6c8ff1f7d4966cadd13f6cbeb06916754bdd1e68e553b58632cbc6d49a154f7629f8fc1bb127513dc848831ae87c6d26503f94c29510d8772ee0e08
6
+ metadata.gz: 1424b3fc448ef0d2a0afac402f87617fc981a179cc5fad4a6883585950f41a885b052680da36ec203bc071fab010c336b9c362b6c72ecbbc5680643f47d938ff
7
+ data.tar.gz: 8702e0d38c802162393f0185b6e485e504203a119115f4d92e43e72b583d32202bf61c6264b7e5df9ae3a6b77ac7526d92d2b21a253ac0893695ef0348109e7f
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']
14
+ ruby-version: ['3.3', '3.4', '4.0']
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v4
@@ -23,10 +23,7 @@ jobs:
23
23
  bundler-cache: true
24
24
 
25
25
  - name: Run tests
26
- run: |
27
- bundle exec ruby -Ilib:test test/heatmap_builder_test.rb
28
- bundle exec ruby -Ilib:test test/calendar_heatmap_builder_test.rb
29
- bundle exec ruby -Ilib:test test/linear_heatmap_builder_test.rb
26
+ run: bundle exec rake test
30
27
 
31
28
  lint:
32
29
  runs-on: ubuntu-latest
data/CHANGELOG.md CHANGED
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.3] - 2026-06-15
11
+
12
+ ### Fixed
13
+ - Calendar month labels now always sit above the first full week of the month.
14
+ Previously, with `month_spacing: 0`, a label could land on the straddling
15
+ column shared by two months; the label position is now derived from each
16
+ week's `week_start`, so straddling weeks defer the label to the next full week.
17
+ - A month is no longer labeled when its only visible week is incomplete (a
18
+ leading or trailing sliver at the edge of the data range). Previously such a
19
+ label could overlap the next month's label when the partial month occupied a
20
+ single column. Labels are now placed only on a fully visible week.
21
+
22
+ ## [0.4.2] - 2026-06-15
23
+
24
+ ### Added
25
+ - New `border_lightness_factor` option controls how the cell border color is derived
26
+ from each cell's color by scaling its OKLCH lightness. Setting it to `1` makes
27
+ the border match the cell color, in which case the (now invisible) border is
28
+ omitted from the SVG entirely. Defaults to `0.9`, preserving previous output.
29
+
30
+ ## [0.4.1] - 2026-06-13
31
+
32
+ ### Changed
33
+ - Default value-to-score conversion now reserves score `0` for empty cells
34
+ (zero or missing values) and maps every non-zero value into the `1..max_score`
35
+ range, so the smallest amount of activity is always visually distinct from an
36
+ empty day. Regenerated `examples/*.svg` to reflect the new bucketing.
37
+ - Auto-calculated `value_min` now anchors on the smallest non-zero value instead
38
+ of zero. Because zero is the reserved empty bucket, this keeps the lightest
39
+ activity color reachable rather than stranding it on values that never occur.
40
+
41
+ ### Fixed
42
+ - Example calendar heatmaps misrendered high-activity days. The generator passed
43
+ raw values straight through as `scores:`, so any value beyond the palette's
44
+ color count wrapped around via modulo (`score_to_color`) and the busiest days
45
+ could render as nearly empty cells. The examples now feed values through the
46
+ bucketing conversion, so cell intensity increases monotonically with the
47
+ underlying value.
48
+
49
+ ## [0.4.0] - 2026-06-12
50
+
51
+ ### Added
52
+ - Tooltip support for calendar cells via the `tooltip:` option. Accepts a callable
53
+ invoked per active cell with `date:`, `score:`, and `value:` keyword arguments;
54
+ the return value becomes the tooltip text.
55
+ - Native SVG `<title>` element is always emitted as a zero-JS browser fallback.
56
+ - `tooltip_attribute:` option (default `"data-tooltip"`) controls which `data-*`
57
+ attribute is written on the cell's `<g>` wrapper for JS tooltip library pickup.
58
+ Set to `nil` to suppress the data attribute and use only the native fallback.
59
+
60
+ ## [0.3.1] - 2026-06-11
61
+
62
+ ### Changed
63
+ - Relaxed `rake` and `minitest` development dependency constraints from `~>` to `>=` to allow future major versions
64
+
65
+ ## [0.3.0] - 2026-06-11
66
+
67
+ ### Changed
68
+ - Dropped support for Ruby 3.0, 3.1, and 3.2 (all EOL); minimum required version is now 3.3
69
+ - Updated all dependencies to latest stable versions
70
+ - Updated Bundler constraint from `~> 2.0` to `>= 2.0`; locked to Bundler 4.0.14
71
+ - CI matrix updated to Ruby 3.3, 3.4, and 4.0
72
+
10
73
  ## [0.2.0] - 2025-10-02
11
74
 
12
75
  ### Added
@@ -32,7 +95,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
95
  - Automatic corner radius clamping to valid range
33
96
 
34
97
  ### Deprecated
35
- - `HeatmapBuilder.generate(scores, options)` - use `HeatmapBuilder.build_linear(scores: scores, **options)` instead
36
98
  - `HeatmapBuilder.generate_calendar(scores, options)` - use `HeatmapBuilder.build_calendar(scores: scores, **options)` instead
37
99
  - Old API still works with deprecation warnings for backward compatibility
38
100
 
@@ -41,13 +103,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
103
  Initial release with core heatmap visualization capabilities.
42
104
 
43
105
  ### Added
44
- - Linear heatmap generation with `HeatmapBuilder.generate()`
45
106
  - Calendar heatmap generation with `HeatmapBuilder.generate_calendar()`
46
107
  - GitHub-style color schemes and styling
47
108
  - Customizable cell size, spacing, colors, and fonts
48
109
  - Support for custom start of week (Monday/Sunday)
49
110
  - SVG output format for perfect scaling
50
111
 
51
- [Unreleased]: https://github.com/dreikanter/heatmap-builder/compare/v0.2.0...HEAD
112
+ [Unreleased]: https://github.com/dreikanter/heatmap-builder/compare/v0.4.3...HEAD
113
+ [0.4.3]: https://github.com/dreikanter/heatmap-builder/compare/v0.4.2...v0.4.3
114
+ [0.4.2]: https://github.com/dreikanter/heatmap-builder/compare/v0.4.1...v0.4.2
115
+ [0.4.1]: https://github.com/dreikanter/heatmap-builder/compare/v0.4.0...v0.4.1
116
+ [0.4.0]: https://github.com/dreikanter/heatmap-builder/compare/v0.3.1...v0.4.0
117
+ [0.3.1]: https://github.com/dreikanter/heatmap-builder/compare/v0.3.0...v0.3.1
118
+ [0.3.0]: https://github.com/dreikanter/heatmap-builder/compare/v0.2.0...v0.3.0
52
119
  [0.2.0]: https://github.com/dreikanter/heatmap-builder/compare/v0.1.0...v0.2.0
53
120
  [0.1.0]: https://github.com/dreikanter/heatmap-builder/releases/tag/v0.1.0
data/Gemfile.lock CHANGED
@@ -1,44 +1,47 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- heatmap-builder (0.2.0)
4
+ heatmap-builder (0.4.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.3)
10
10
  docile (1.4.1)
11
- json (2.14.1)
11
+ drb (2.2.3)
12
+ json (2.19.9)
12
13
  language_server-protocol (3.17.0.5)
13
14
  lint_roller (1.1.0)
14
- minitest (5.25.5)
15
- parallel (1.27.0)
16
- parser (3.3.9.0)
15
+ minitest (6.0.6)
16
+ drb (~> 2.0)
17
+ prism (~> 1.5)
18
+ parallel (2.1.0)
19
+ parser (3.3.11.1)
17
20
  ast (~> 2.4.1)
18
21
  racc
19
- prism (1.5.1)
22
+ prism (1.9.0)
20
23
  racc (1.8.1)
21
24
  rainbow (3.1.1)
22
- rake (13.3.0)
23
- regexp_parser (2.11.3)
24
- rubocop (1.80.2)
25
+ rake (13.4.2)
26
+ regexp_parser (2.12.0)
27
+ rubocop (1.87.0)
25
28
  json (~> 2.3)
26
29
  language_server-protocol (~> 3.17.0.2)
27
30
  lint_roller (~> 1.1.0)
28
- parallel (~> 1.10)
31
+ parallel (>= 1.10)
29
32
  parser (>= 3.3.0.2)
30
33
  rainbow (>= 2.2.2, < 4.0)
31
34
  regexp_parser (>= 2.9.3, < 3.0)
32
- rubocop-ast (>= 1.46.0, < 2.0)
35
+ rubocop-ast (>= 1.49.0, < 2.0)
33
36
  ruby-progressbar (~> 1.7)
34
37
  unicode-display_width (>= 2.4.0, < 4.0)
35
- rubocop-ast (1.46.0)
38
+ rubocop-ast (1.49.1)
36
39
  parser (>= 3.3.7.2)
37
- prism (~> 1.4)
38
- rubocop-performance (1.25.0)
40
+ prism (~> 1.7)
41
+ rubocop-performance (1.26.1)
39
42
  lint_roller (~> 1.1)
40
43
  rubocop (>= 1.75.0, < 2.0)
41
- rubocop-ast (>= 1.38.0, < 2.0)
44
+ rubocop-ast (>= 1.47.1, < 2.0)
42
45
  ruby-progressbar (1.13.0)
43
46
  simplecov (0.22.0)
44
47
  docile (~> 1.1)
@@ -46,33 +49,33 @@ GEM
46
49
  simplecov_json_formatter (~> 0.1)
47
50
  simplecov-html (0.13.2)
48
51
  simplecov_json_formatter (0.1.4)
49
- standard (1.51.1)
52
+ standard (1.55.0)
50
53
  language_server-protocol (~> 3.17.0.2)
51
54
  lint_roller (~> 1.0)
52
- rubocop (~> 1.80.2)
55
+ rubocop (~> 1.87.0)
53
56
  standard-custom (~> 1.0.0)
54
57
  standard-performance (~> 1.8)
55
58
  standard-custom (1.0.2)
56
59
  lint_roller (~> 1.0)
57
60
  rubocop (~> 1.50)
58
- standard-performance (1.8.0)
61
+ standard-performance (1.9.0)
59
62
  lint_roller (~> 1.1)
60
- rubocop-performance (~> 1.25.0)
63
+ rubocop-performance (~> 1.26.0)
61
64
  unicode-display_width (3.2.0)
62
65
  unicode-emoji (~> 4.1)
63
- unicode-emoji (4.1.0)
66
+ unicode-emoji (4.2.0)
64
67
 
65
68
  PLATFORMS
66
69
  arm64-darwin-24
67
70
  ruby
68
71
 
69
72
  DEPENDENCIES
70
- bundler (~> 2.0)
73
+ bundler (>= 2.0)
71
74
  heatmap-builder!
72
- minitest (~> 5.0)
73
- rake (~> 13.0)
75
+ minitest (>= 5.0)
76
+ rake (>= 13.0)
74
77
  simplecov (~> 0.22)
75
78
  standard (~> 1.0)
76
79
 
77
80
  BUNDLED WITH
78
- 2.5.22
81
+ 4.0.9
data/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # HeatmapBuilder
2
2
 
3
- A Ruby gem that generates embeddable SVG heatmap visualizations with GitHub-style calendar layouts and linear progress indicators. Perfect for Rails applications and any project that needs to display activity data in a visual format.
3
+ 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.
4
4
 
5
5
  ![GitHub-style Calendar](examples/calendar_github_style.svg)
6
6
 
7
7
  ## Features
8
8
 
9
9
  - GitHub-style calendar layouts for date-based data.
10
- - Linear heatmaps.
11
10
  - Vector-based output (SVG) for crisp rendering at any resolution.
12
11
  - Optional numeric values displayed in each cell.
13
12
  - **Use pre-calculated scores or raw numeric values** - automatic mapping to color scales.
@@ -16,6 +15,7 @@ A Ruby gem that generates embeddable SVG heatmap visualizations with GitHub-styl
16
15
  - Rounded corners (and circular cells, if you're into that kind of thing).
17
16
  - Dynamic palette generation from two colors or manually-specified colors.
18
17
  - OKLCH color interpolation for clean color transitions and perceptual uniformity.
18
+ - Tooltip support with native browser fallback and JS library integration hooks.
19
19
  - **Zero dependencies.**
20
20
 
21
21
  ## Installation
@@ -36,29 +36,14 @@ Or install it yourself as:
36
36
 
37
37
  ## Usage
38
38
 
39
- ### Linear Heatmaps
40
-
41
- ```ruby
42
- require 'heatmap-builder'
43
-
44
- # Generate SVG for daily scores
45
- scores = [0, 1, 2, 3, 4, 5, 2, 1]
46
- svg = HeatmapBuilder.build_linear(scores: scores)
47
-
48
- # In a Rails view
49
- <%= raw HeatmapBuilder.build_linear(scores: @daily_scores) %>
50
- ```
51
-
52
- ![Weekly Progress](examples/weekly_progress.svg)
53
-
54
39
  ### Calendar Heatmaps
55
40
 
56
41
  ```ruby
57
42
  # GitHub-style calendar heatmap
58
43
  scores_by_date = {
59
- '2024-01-01' => 2,
60
- '2024-01-02' => 4,
61
- '2024-01-03' => 1,
44
+ '2026-01-01' => 2,
45
+ '2026-01-02' => 4,
46
+ '2026-01-03' => 1,
62
47
  # ... more dates
63
48
  }
64
49
 
@@ -67,34 +52,6 @@ svg = HeatmapBuilder.build_calendar(scores: scores_by_date)
67
52
 
68
53
  ![GitHub-style Calendar](examples/calendar_github_style.svg)
69
54
 
70
- ### Linear Heatmap Options
71
-
72
- You must provide either `scores:` or `values:` (but not both). All other options are optional keyword arguments with sensible defaults.
73
-
74
- **Data options:**
75
-
76
- - `scores` - Array of pre-calculated scores (integers from 0 to number of colors minus 1). Required if `values` is not provided.
77
- - `values` - Array of arbitrary numeric values to be automatically mapped to scores. Required if `scores` is not provided. See [Using Raw Values Instead of Scores](#using-raw-values-instead-of-scores).
78
-
79
- **Value-to-score conversion options** (only used with `values:`):
80
-
81
- - `value_min` - Minimum boundary for value-to-score mapping. Defaults to the minimum value in your data.
82
- - `value_max` - Maximum boundary for value-to-score mapping. Defaults to the maximum value in your data.
83
- - `value_to_score` - Custom callable for value-to-score conversion. Receives `value:`, `index:`, `min:`, `max:`, `max_score:` parameters and must return an integer between 0 and `max_score`. See [Custom Scoring Logic](#custom-scoring-logic) for details.
84
-
85
- **Appearance options:**
86
-
87
- - `cell_size` - Size of each square in pixels. Defaults to 10.
88
- - `cell_spacing` - Space between squares in pixels. Defaults to 1.
89
- - `font_size` - Font size for score text in pixels. Defaults to 8.
90
- - `border_width` - Border width around each cell in pixels. Defaults to 1.
91
- - `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.
92
- - `text_color` - Color of score text as a hex string. Defaults to `"#000000"` (black).
93
-
94
- **Color options:**
95
-
96
- - `colors` - Color palette for the heatmap. Can be a predefined palette constant (e.g., `HeatmapBuilder::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::GITHUB_GREEN`. See [Predefined Color Palettes](#predefined-color-palettes) and [Dynamic Palettes Generation](#dynamic-palettes-generation).
97
-
98
55
  ### Calendar Heatmap Options
99
56
 
100
57
  You must provide either `scores:` or `values:` (but not both). All other options are optional keyword arguments with sensible defaults.
@@ -116,21 +73,26 @@ You must provide either `scores:` or `values:` (but not both). All other options
116
73
  - `cell_spacing` - Space between squares in pixels. Defaults to 1.
117
74
  - `font_size` - Font size for labels in pixels. Defaults to 8.
118
75
  - `border_width` - Border width around each cell in pixels. Defaults to 1.
76
+ - `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.
119
77
  - `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.
120
- - `text_color` - Color of label text as a hex string. Defaults to `"#000000"` (black).
121
78
 
122
79
  **Color options:**
123
80
 
124
- - `colors` - Color palette for the heatmap. Can be a predefined palette constant (e.g., `HeatmapBuilder::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::GITHUB_GREEN`. See [Predefined Color Palettes](#predefined-color-palettes) and [Dynamic Palettes Generation](#dynamic-palettes-generation).
81
+ - `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](#predefined-color-palettes) and [Dynamic Palettes Generation](#dynamic-palettes-generation).
125
82
 
126
83
  **Calendar-specific options:**
127
84
 
128
85
  - `start_of_week` - First day of the week. One of `:sunday`, `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`, `:saturday`. Defaults to `:monday`.
129
- - `month_spacing` - Extra horizontal space between months in pixels. Defaults to 5.
86
+ - `month_spacing` - Extra horizontal space between months in pixels. Defaults to 0.
130
87
  - `show_month_labels` - Show month names at the top of the calendar. Defaults to `true`.
131
88
  - `show_day_labels` - Show day abbreviations on the left side of the calendar. Defaults to `true`.
132
89
  - `show_outside_cells` - Show cells outside the date range with inactive styling. Defaults to `false`.
133
90
 
91
+ **Tooltip options:**
92
+
93
+ - `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).
94
+ - `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](#tooltips).
95
+
134
96
  **Internationalization options:**
135
97
 
136
98
  - `day_labels` - Array of day abbreviations starting from Sunday (7 elements). Defaults to `%w[S M T W T F S]`. See [I18n](#i18n).
@@ -143,19 +105,11 @@ A **score** is an integer (0 to N-1) that maps directly to a color in your palet
143
105
  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:
144
106
 
145
107
  ```ruby
146
- # Linear heatmap with automatic score calculation
147
- values = [10, 25, 50, 75, 100]
148
- svg = HeatmapBuilder.build_linear(
149
- values: values,
150
- value_min: 0, # Optional: explicitly set minimum (defaults to actual min)
151
- value_max: 100 # Optional: explicitly set maximum (defaults to actual max)
152
- )
153
-
154
108
  # Calendar heatmap with automatic score calculation
155
109
  values_by_date = {
156
- Date.new(2024, 1, 1) => 45.2,
157
- Date.new(2024, 1, 2) => 78.5,
158
- Date.new(2024, 1, 3) => 12.0
110
+ Date.new(2026, 1, 1) => 45.2,
111
+ Date.new(2026, 1, 2) => 78.5,
112
+ Date.new(2026, 1, 3) => 12.0
159
113
  }
160
114
 
161
115
  svg = HeatmapBuilder.build_calendar(
@@ -177,30 +131,17 @@ By default, values are mapped to scores using linear distribution. You can provi
177
131
 
178
132
  The callable receives these parameters:
179
133
  - `value:` - The current value being converted
180
- - `index:` or `date:` - The position in the data (linear heatmaps use `index:`, calendar heatmaps use `date:`)
134
+ - `date:` - The date for the data point
181
135
  - `min:` - The minimum boundary value
182
136
  - `max:` - The maximum boundary value
183
137
  - `max_score:` - The maximum valid score (color palette length minus 1)
184
138
 
185
139
  The function must return an integer between 0 and `max_score`.
186
140
 
187
- Custom scoring logic - linear distribution example:
141
+ Custom scoring logic - logarithmic scale for data with wide range (e.g., 1 to 10000):
188
142
 
189
143
  ```ruby
190
- linear_formula = ->(value:, index:, min:, max:, max_score:) {
191
- ((value - min) / (max - min) * max_score).round
192
- }
193
-
194
- svg = HeatmapBuilder.build_linear(
195
- values: [10, 20, 30],
196
- value_to_score: linear_formula
197
- )
198
- ```
199
-
200
- Logarithmic scale for data with wide range (e.g., 1 to 10000):
201
-
202
- ```ruby
203
- logarithmic_formula = ->(value:, index:, min:, max:, max_score:) {
144
+ logarithmic_formula = ->(value:, date:, min:, max:, max_score:) {
204
145
  return 0 if value <= 0 || min <= 0
205
146
 
206
147
  log_value = Math.log10(value)
@@ -210,8 +151,8 @@ logarithmic_formula = ->(value:, index:, min:, max:, max_score:) {
210
151
  ((log_value - log_min) / (log_max - log_min) * max_score).round.clamp(0, max_score)
211
152
  }
212
153
 
213
- svg = HeatmapBuilder.build_linear(
214
- values: [1, 10, 100, 1000, 10000],
154
+ svg = HeatmapBuilder.build_calendar(
155
+ values: values_by_date,
215
156
  value_to_score: logarithmic_formula
216
157
  )
217
158
  ```
@@ -221,13 +162,7 @@ svg = HeatmapBuilder.build_linear(
221
162
  #### GitHub Green (Default)
222
163
 
223
164
  ```ruby
224
- HeatmapBuilder.build_linear(scores: scores, colors: HeatmapBuilder::GITHUB_GREEN)
225
- ```
226
-
227
- ![GitHub Green Linear](examples/linear_github_green.svg)
228
-
229
- ```ruby
230
- HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::GITHUB_GREEN)
165
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::GITHUB_GREEN)
231
166
  ```
232
167
 
233
168
  ![Default Calendar](examples/calendar_default.svg)
@@ -235,13 +170,7 @@ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::GIT
235
170
  #### Blue Ocean
236
171
 
237
172
  ```ruby
238
- HeatmapBuilder.build_linear(scores: scores, colors: HeatmapBuilder::BLUE_OCEAN)
239
- ```
240
-
241
- ![Blue Ocean Linear](examples/linear_blue_ocean.svg)
242
-
243
- ```ruby
244
- HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::BLUE_OCEAN)
173
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::BLUE_OCEAN)
245
174
  ```
246
175
 
247
176
  ![Blue Ocean Calendar](examples/calendar_blue_ocean.svg)
@@ -249,13 +178,7 @@ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::BLU
249
178
  #### Warm Sunset
250
179
 
251
180
  ```ruby
252
- HeatmapBuilder.build_linear(scores: scores, colors: HeatmapBuilder::WARM_SUNSET)
253
- ```
254
-
255
- ![Warm Sunset Linear](examples/linear_warm_sunset.svg)
256
-
257
- ```ruby
258
- HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::WARM_SUNSET)
181
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::WARM_SUNSET)
259
182
  ```
260
183
 
261
184
  ![Warm Sunset Calendar](examples/calendar_warm_sunset.svg)
@@ -263,13 +186,7 @@ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::WAR
263
186
  #### Purple Vibes
264
187
 
265
188
  ```ruby
266
- HeatmapBuilder.build_linear(scores: scores, colors: HeatmapBuilder::PURPLE_VIBES)
267
- ```
268
-
269
- ![Purple Vibes Linear](examples/linear_purple_vibes.svg)
270
-
271
- ```ruby
272
- HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::PURPLE_VIBES)
189
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::PURPLE_VIBES)
273
190
  ```
274
191
 
275
192
  ![Purple Vibes Calendar](examples/calendar_purple_vibes.svg)
@@ -277,13 +194,7 @@ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::PUR
277
194
  #### Red to Green
278
195
 
279
196
  ```ruby
280
- HeatmapBuilder.build_linear(scores: scores, colors: HeatmapBuilder::RED_TO_GREEN)
281
- ```
282
-
283
- ![Red to Green Linear](examples/linear_red_to_green.svg)
284
-
285
- ```ruby
286
- HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::RED_TO_GREEN)
197
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::RED_TO_GREEN)
287
198
  ```
288
199
 
289
200
  ![Red to Green Calendar](examples/calendar_red_to_green.svg)
@@ -300,44 +211,46 @@ neon_gradient = {
300
211
  steps: 5
301
212
  }
302
213
 
303
- svg = HeatmapBuilder.build_linear(scores: scores, colors: neon_gradient)
214
+ svg = HeatmapBuilder.build_calendar(scores: calendar_data, colors: neon_gradient)
304
215
  ```
305
216
 
306
- ![Neon Gradient Linear](examples/linear_neon_gradient.svg)
307
-
308
217
  The OKLCH color space ensures perceptually uniform color transitions, making gradients appear smooth and natural to the human eye.
309
218
 
310
- ### Rounded Corners
219
+ ### Cell Borders
311
220
 
312
- Both linear and calendar heatmaps support rounded corners using the `corner_radius` option.
221
+ 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).
313
222
 
314
- A typical value is around 2 pixels for a subtle rounded effect:
223
+ 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.
315
224
 
316
225
  ```ruby
317
- # Linear heatmap with rounded corners
318
- HeatmapBuilder.build_linear(
319
- scores: scores,
320
- corner_radius: 2,
321
- cell_size: 18
226
+ HeatmapBuilder.build_calendar(
227
+ scores: calendar_data,
228
+ border_width: 1,
229
+ border_lightness_factor: 0.7
322
230
  )
323
231
  ```
324
232
 
325
- ![Linear Rounded Corners](examples/linear_rounded_corners.svg)
233
+ ![Calendar with Cell Borders](examples/calendar_cell_borders.svg)
326
234
 
327
- 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)`). Maximum radius values render circular cells:
235
+ With `border_lightness_factor` set to `1`, the border is omitted entirely, leaving cells edge to edge:
328
236
 
329
237
  ```ruby
330
- # Linear heatmap with max radius rounded corners - circular cells
331
- HeatmapBuilder.build_linear(
332
- scores: scores,
333
- corner_radius: 9,
334
- cell_size: 18
238
+ HeatmapBuilder.build_calendar(
239
+ scores: calendar_data,
240
+ border_width: 1,
241
+ border_lightness_factor: 1
335
242
  )
336
243
  ```
337
244
 
338
- ![Linear Rounded Corners](examples/linear_rounded_corners_max_radius.svg)
245
+ ![Calendar with No Cell Borders](examples/calendar_no_borders.svg)
246
+
247
+ ### Rounded Corners
339
248
 
340
- Calendar heatmap examples:
249
+ Calendar heatmaps support rounded corners using the `corner_radius` option.
250
+
251
+ 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)`).
252
+
253
+ A typical value is around 2 pixels for a subtle rounded effect:
341
254
 
342
255
  ```ruby
343
256
  # Calendar heatmap with rounded corners
@@ -350,6 +263,8 @@ HeatmapBuilder.build_calendar(
350
263
 
351
264
  ![Calendar Rounded Corners](examples/calendar_rounded_corners.svg)
352
265
 
266
+ Maximum radius values render circular cells:
267
+
353
268
  ```ruby
354
269
  # Calendar heatmap with max radius rounded corners - circular cells
355
270
  HeatmapBuilder.build_calendar(
@@ -361,6 +276,61 @@ HeatmapBuilder.build_calendar(
361
276
 
362
277
  ![Calendar Rounded Corners](examples/calendar_rounded_corners_max_radius.svg)
363
278
 
279
+ ### Month Spacing
280
+
281
+ Add visual separation between months using the `month_spacing` option. This adds horizontal gaps at month boundaries, making it easier to distinguish individual months:
282
+
283
+ ```ruby
284
+ HeatmapBuilder.build_calendar(
285
+ scores: calendar_data,
286
+ cell_size: 14,
287
+ month_spacing: 10,
288
+ corner_radius: 3
289
+ )
290
+ ```
291
+
292
+ ![Calendar with Month Spacing](examples/calendar_month_spacing_rounded.svg)
293
+
294
+ ### Tooltips
295
+
296
+ 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.
297
+
298
+ ```ruby
299
+ HeatmapBuilder.build_calendar(
300
+ scores: calendar_data,
301
+ tooltip: ->(date:, score:, value: nil) { "#{date.strftime('%b %-d')}: #{score} contributions" }
302
+ )
303
+ ```
304
+
305
+ 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.
306
+
307
+ ```xml
308
+ <g data-tooltip="Jan 15: 4 contributions">
309
+ <title>Jan 15: 4 contributions</title>
310
+ <rect fill="#40c463" .../>
311
+ </g>
312
+ ```
313
+
314
+ 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:
315
+
316
+ ```js
317
+ // Tippy.js — one line of initialization
318
+ tippy('[data-tooltip]', { content: el => el.dataset.tooltip })
319
+ ```
320
+
321
+ 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:
322
+
323
+ ```ruby
324
+ # Matches data-tippy-content used by some Tippy.js configurations
325
+ HeatmapBuilder.build_calendar(
326
+ scores: calendar_data,
327
+ tooltip: ->(date:, score:, value: nil) { "#{date}: #{score}" },
328
+ tooltip_attribute: "data-tippy-content"
329
+ )
330
+ ```
331
+
332
+ Outside cells rendered via `show_outside_cells: true` never receive tooltip markup.
333
+
364
334
  ### I18n
365
335
 
366
336
  Calendar heatmaps support internationalization by customizing the `day_labels` and `month_labels` options: