heatmap-builder 0.1.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.
data/README.md CHANGED
@@ -1,17 +1,22 @@
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.
10
+ - Vector-based output (SVG) for crisp rendering at any resolution.
11
+ - Optional numeric values displayed in each cell.
12
+ - **Use pre-calculated scores or raw numeric values** - automatic mapping to color scales.
13
+ - Custom value-to-score conversion functions for advanced scoring logic.
11
14
  - Parametric everything: customize cell size, spacing, colors, fonts, etc.
12
- - Shows numeric values in each cell.
15
+ - Rounded corners (and circular cells, if you're into that kind of thing).
16
+ - Dynamic palette generation from two colors or manually-specified colors.
17
+ - OKLCH color interpolation for clean color transitions and perceptual uniformity.
18
+ - Tooltip support with native browser fallback and JS library integration hooks.
13
19
  - **Zero dependencies.**
14
- - Responsive: SVG format scales perfectly at any size
15
20
 
16
21
  ## Installation
17
22
 
@@ -31,90 +36,358 @@ Or install it yourself as:
31
36
 
32
37
  ## Usage
33
38
 
34
- ### Linear Heatmaps
39
+ ### Calendar Heatmaps
35
40
 
36
41
  ```ruby
37
- require 'heatmap-builder'
42
+ # GitHub-style calendar heatmap
43
+ scores_by_date = {
44
+ '2026-01-01' => 2,
45
+ '2026-01-02' => 4,
46
+ '2026-01-03' => 1,
47
+ # ... more dates
48
+ }
49
+
50
+ svg = HeatmapBuilder.build_calendar(scores: scores_by_date)
51
+ ```
52
+
53
+ ![GitHub-style Calendar](examples/calendar_github_style.svg)
54
+
55
+ ### Calendar Heatmap Options
56
+
57
+ You must provide either `scores:` or `values:` (but not both). All other options are optional keyword arguments with sensible defaults.
58
+
59
+ **Data options:**
60
+
61
+ - `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.
62
+ - `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](#using-raw-values-instead-of-scores).
63
+
64
+ **Value-to-score conversion options** (only used with `values:`):
65
+
66
+ - `value_min` - Minimum boundary for value-to-score mapping. Defaults to the minimum value in your data.
67
+ - `value_max` - Maximum boundary for value-to-score mapping. Defaults to the maximum value in your data.
68
+ - `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](#custom-scoring-logic) for details.
38
69
 
39
- # Generate SVG for daily scores
40
- scores = [0, 1, 2, 3, 4, 5, 2, 1]
41
- svg = HeatmapBuilder.generate(scores)
70
+ **Appearance options:**
71
+
72
+ - `cell_size` - Size of each square in pixels. Defaults to 12.
73
+ - `cell_spacing` - Space between squares in pixels. Defaults to 1.
74
+ - `font_size` - Font size for labels in pixels. Defaults to 8.
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.
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.
78
+
79
+ **Color options:**
80
+
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).
82
+
83
+ **Calendar-specific options:**
84
+
85
+ - `start_of_week` - First day of the week. One of `:sunday`, `:monday`, `:tuesday`, `:wednesday`, `:thursday`, `:friday`, `:saturday`. Defaults to `:monday`.
86
+ - `month_spacing` - Extra horizontal space between months in pixels. Defaults to 0.
87
+ - `show_month_labels` - Show month names at the top of the calendar. Defaults to `true`.
88
+ - `show_day_labels` - Show day abbreviations on the left side of the calendar. Defaults to `true`.
89
+ - `show_outside_cells` - Show cells outside the date range with inactive styling. Defaults to `false`.
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
+
96
+ **Internationalization options:**
97
+
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).
99
+ - `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](#i18n).
100
+
101
+ ### Using Raw Values Instead of Scores
102
+
103
+ 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.
104
+
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:
106
+
107
+ ```ruby
108
+ # Calendar heatmap with automatic score calculation
109
+ values_by_date = {
110
+ Date.new(2026, 1, 1) => 45.2,
111
+ Date.new(2026, 1, 2) => 78.5,
112
+ Date.new(2026, 1, 3) => 12.0
113
+ }
42
114
 
43
- # In a Rails view
44
- <%= raw HeatmapBuilder.generate(@daily_scores) %>
115
+ svg = HeatmapBuilder.build_calendar(
116
+ values: values_by_date,
117
+ value_min: 0,
118
+ value_max: 100
119
+ )
45
120
  ```
46
121
 
47
- ![Weekly Progress](examples/weekly_progress.svg)
122
+ The builder will automatically:
123
+ - Calculate min/max boundaries from your data if not specified
124
+ - Map values to color scores using linear distribution
125
+ - Clamp values outside the boundaries
126
+ - Handle nil values by treating them as the minimum boundary
48
127
 
49
- ### Calendar Heatmaps
128
+ ### Custom Scoring Logic
129
+
130
+ 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.
131
+
132
+ The callable receives these parameters:
133
+ - `value:` - The current value being converted
134
+ - `date:` - The date for the data point
135
+ - `min:` - The minimum boundary value
136
+ - `max:` - The maximum boundary value
137
+ - `max_score:` - The maximum valid score (color palette length minus 1)
138
+
139
+ The function must return an integer between 0 and `max_score`.
140
+
141
+ Custom scoring logic - logarithmic scale for data with wide range (e.g., 1 to 10000):
50
142
 
51
143
  ```ruby
52
- # GitHub-style calendar heatmap
53
- scores_by_date = {
54
- '2024-01-01' => 2,
55
- '2024-01-02' => 4,
56
- '2024-01-03' => 1,
57
- # ... more dates
144
+ logarithmic_formula = ->(value:, date:, min:, max:, max_score:) {
145
+ return 0 if value <= 0 || min <= 0
146
+
147
+ log_value = Math.log10(value)
148
+ log_min = Math.log10(min)
149
+ log_max = Math.log10(max)
150
+
151
+ ((log_value - log_min) / (log_max - log_min) * max_score).round.clamp(0, max_score)
58
152
  }
59
153
 
60
- svg = HeatmapBuilder.generate_calendar(scores_by_date)
154
+ svg = HeatmapBuilder.build_calendar(
155
+ values: values_by_date,
156
+ value_to_score: logarithmic_formula
157
+ )
158
+ ```
159
+
160
+ ### Predefined Color Palettes
161
+
162
+ #### GitHub Green (Default)
163
+
164
+ ```ruby
165
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::GITHUB_GREEN)
166
+ ```
167
+
168
+ ![Default Calendar](examples/calendar_default.svg)
169
+
170
+ #### Blue Ocean
171
+
172
+ ```ruby
173
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::BLUE_OCEAN)
174
+ ```
175
+
176
+ ![Blue Ocean Calendar](examples/calendar_blue_ocean.svg)
177
+
178
+ #### Warm Sunset
179
+
180
+ ```ruby
181
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::WARM_SUNSET)
182
+ ```
183
+
184
+ ![Warm Sunset Calendar](examples/calendar_warm_sunset.svg)
185
+
186
+ #### Purple Vibes
187
+
188
+ ```ruby
189
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::PURPLE_VIBES)
190
+ ```
191
+
192
+ ![Purple Vibes Calendar](examples/calendar_purple_vibes.svg)
193
+
194
+ #### Red to Green
195
+
196
+ ```ruby
197
+ HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::RED_TO_GREEN)
61
198
  ```
62
199
 
63
- ### Custom Configuration
200
+ ![Red to Green Calendar](examples/calendar_red_to_green.svg)
201
+
202
+ ### Dynamic Palettes Generation
203
+
204
+ Generate custom color palettes from any two colors using OKLCH color space for superior color interpolation:
64
205
 
65
206
  ```ruby
66
- # Customize linear heatmap appearance
67
- options = {
68
- cell_size: 35, # Size of each square (default: 20)
69
- cell_spacing: 1, # Space between squares (default: 2)
70
- font_size: 20, # Font size for score text (default: 12)
71
- colors: %w[ # Custom color palette (default: GitHub-style)
72
- #f0f0f0
73
- #c6e48b
74
- #7bc96f
75
- #239a3b
76
- #196127
77
- ]
207
+ # Generate a 5-step palette from electric cyan to hot magenta
208
+ neon_gradient = {
209
+ from: "#00FFFF",
210
+ to: "#FF1493",
211
+ steps: 5
78
212
  }
79
213
 
80
- svg = HeatmapBuilder.generate([1, 2, 3, 4, 5, 6, 7], options)
214
+ svg = HeatmapBuilder.build_calendar(scores: calendar_data, colors: neon_gradient)
215
+ ```
216
+
217
+ The OKLCH color space ensures perceptually uniform color transitions, making gradients appear smooth and natural to the human eye.
218
+
219
+ ### Cell Borders
220
+
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).
222
+
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.
224
+
225
+ ```ruby
226
+ HeatmapBuilder.build_calendar(
227
+ scores: calendar_data,
228
+ border_width: 1,
229
+ border_lightness_factor: 0.7
230
+ )
231
+ ```
232
+
233
+ ![Calendar with Cell Borders](examples/calendar_cell_borders.svg)
234
+
235
+ With `border_lightness_factor` set to `1`, the border is omitted entirely, leaving cells edge to edge:
236
+
237
+ ```ruby
238
+ HeatmapBuilder.build_calendar(
239
+ scores: calendar_data,
240
+ border_width: 1,
241
+ border_lightness_factor: 1
242
+ )
81
243
  ```
82
244
 
83
- ![Large Cells](examples/large_cells.svg)
245
+ ![Calendar with No Cell Borders](examples/calendar_no_borders.svg)
246
+
247
+ ### Rounded Corners
248
+
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:
84
254
 
85
255
  ```ruby
86
- # Calendar heatmap options
87
- calendar_options = {
256
+ # Calendar heatmap with rounded corners
257
+ HeatmapBuilder.build_calendar(
258
+ scores: calendar_data,
259
+ corner_radius: 2,
260
+ cell_size: 14
261
+ )
262
+ ```
263
+
264
+ ![Calendar Rounded Corners](examples/calendar_rounded_corners.svg)
265
+
266
+ Maximum radius values render circular cells:
267
+
268
+ ```ruby
269
+ # Calendar heatmap with max radius rounded corners - circular cells
270
+ HeatmapBuilder.build_calendar(
271
+ scores: calendar_data,
272
+ corner_radius: 7,
273
+ cell_size: 14
274
+ )
275
+ ```
276
+
277
+ ![Calendar Rounded Corners](examples/calendar_rounded_corners_max_radius.svg)
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,
88
286
  cell_size: 14,
89
- start_of_week: :sunday, # :monday (default) or :sunday
90
- show_outside_cells: true # Show cells outside date range
91
- }
287
+ month_spacing: 10,
288
+ corner_radius: 3
289
+ )
290
+ ```
291
+
292
+ ![Calendar with Month Spacing](examples/calendar_month_spacing_rounded.svg)
92
293
 
93
- svg = HeatmapBuilder.generate_calendar(scores_by_date, calendar_options)
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
+ )
94
303
  ```
95
304
 
96
- ![Calendar with Sunday Start](examples/calendar_sunday_start.svg)
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.
97
306
 
98
- ### Color Mapping
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
+ ```
99
320
 
100
- - Score `0`: Uses the first color (typically light gray)
101
- - Score `1+`: Cycles through remaining colors based on score value
102
- - Higher scores automatically map to available colors in the palette
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
+
334
+ ### I18n
335
+
336
+ Calendar heatmaps support internationalization by customizing the `day_labels` and `month_labels` options:
337
+
338
+ ```ruby
339
+ # French calendar
340
+ HeatmapBuilder.build_calendar(
341
+ scores: calendar_data,
342
+ day_labels: %w[D L M M J V S], # Dimanche, Lundi, Mardi, etc.
343
+ month_labels: %w[Jan Fév Mar Avr Mai Jun Jul Aoû Sep Oct Nov Déc]
344
+ )
345
+
346
+ # German calendar
347
+ HeatmapBuilder.build_calendar(
348
+ scores: calendar_data,
349
+ day_labels: %w[S M D M D F S], # Sonntag, Montag, Dienstag, etc.
350
+ month_labels: %w[Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez]
351
+ )
352
+
353
+ # Italian calendar
354
+ HeatmapBuilder.build_calendar(
355
+ scores: calendar_data,
356
+ day_labels: %w[D L M M G V S], # Domenica, Lunedì, Martedì, etc.
357
+ month_labels: %w[Gen Feb Mar Apr Mag Giu Lug Ago Set Ott Nov Dic]
358
+ )
359
+
360
+ # Spanish calendar
361
+ HeatmapBuilder.build_calendar(
362
+ scores: calendar_data,
363
+ day_labels: %w[D L M X J V S], # Domingo, Lunes, Martes, etc.
364
+ month_labels: %w[Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic]
365
+ )
366
+ ```
367
+
368
+ The `day_labels` array should contain 7 elements starting from Sunday, and `month_labels` should contain 12 elements for January through December.
103
369
 
104
370
  ## Development
105
371
 
106
- After checking out the repo, run `bin/setup` to install dependencies. Run tests with:
372
+ After checking out the repo, run `bin/setup` to install development dependencies.
373
+
374
+ ### Running Tests
107
375
 
108
376
  ```bash
109
- ruby -Ilib:test test/heatmap_builder_test.rb
110
- ```
377
+ # Run all tests
378
+ rake test
379
+
380
+ # Run tests with code linting
381
+ rake
111
382
 
112
- To install this gem onto your local machine, run `bundle exec rake install`.
383
+ # Update test snapshots after making intentional changes to output
384
+ rake update_snapshots
385
+ ```
113
386
 
114
387
  To generate all example SVG files you see in this readme:
115
388
 
116
389
  ```bash
117
- ruby examples/generate_samples.rb
390
+ bin/generate_examples
118
391
  ```
119
392
 
120
393
  ## Contributing
@@ -127,4 +400,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
127
400
 
128
401
  ## Code of Conduct
129
402
 
130
- Everyone interacting in the HeatmapBuilder project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dreikanter/heatmap-builder/blob/master/CODE_OF_CONDUCT.md).
403
+ Everyone interacting in the HeatmapBuilder project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dreikanter/heatmap-builder/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -13,4 +13,19 @@ rescue LoadError
13
13
  # standard is not available
14
14
  end
15
15
 
16
+ desc "Update test snapshots"
17
+ task :update_snapshots do
18
+ require "fileutils"
19
+ snapshots_dir = File.expand_path("test/snapshots", __dir__)
20
+
21
+ if Dir.exist?(snapshots_dir)
22
+ puts "Removing existing snapshots..."
23
+ FileUtils.rm_rf(Dir["#{snapshots_dir}/*"])
24
+ end
25
+
26
+ puts "Regenerating snapshots..."
27
+ ENV["UPDATE_SNAPSHOTS"] = "1"
28
+ Rake::Task[:test].invoke
29
+ end
30
+
16
31
  task default: [:standard, :test]
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "date"
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "heatmap-builder"
6
+
7
+ EXAMPLES_DIR = File.expand_path("../examples", __dir__)
8
+
9
+ def example_path(filename)
10
+ File.join(EXAMPLES_DIR, filename)
11
+ end
12
+
13
+ def generate_example(filename, description, force: false, &block)
14
+ puts "Generating #{description}..."
15
+
16
+ filepath = example_path(filename)
17
+ puts File.basename(filepath)
18
+
19
+ if File.exist?(filepath) && !force
20
+ puts "skipped - already exists"
21
+ else
22
+ svg = block.call
23
+ File.write(filepath, svg)
24
+ puts "created"
25
+ end
26
+
27
+ puts
28
+ end
29
+
30
+ def generate_sample_svgs(force: false)
31
+ Dir.mkdir(EXAMPLES_DIR) unless Dir.exist?(EXAMPLES_DIR)
32
+
33
+ calendar_data = sample_calendar_data
34
+
35
+ generate_example("calendar_github_style.svg", "GitHub-style calendar", force: force) do
36
+ HeatmapBuilder.build_calendar(
37
+ values: calendar_data,
38
+ cell_size: 14,
39
+ month_spacing: 0
40
+ )
41
+ end
42
+
43
+ generate_example("calendar_default.svg", "default colors calendar", force: force) do
44
+ HeatmapBuilder.build_calendar(
45
+ values: calendar_data,
46
+ cell_size: 14,
47
+ month_spacing: 0
48
+ )
49
+ end
50
+
51
+ generate_example("calendar_blue_ocean.svg", "calendar with Blue Ocean palette", force: force) do
52
+ HeatmapBuilder.build_calendar(
53
+ values: calendar_data,
54
+ colors: HeatmapBuilder::Calendar::BLUE_OCEAN,
55
+ cell_size: 14,
56
+ month_spacing: 0
57
+ )
58
+ end
59
+
60
+ generate_example("calendar_warm_sunset.svg", "calendar with Warm Sunset palette", force: force) do
61
+ HeatmapBuilder.build_calendar(
62
+ values: calendar_data,
63
+ colors: HeatmapBuilder::Calendar::WARM_SUNSET,
64
+ cell_size: 14,
65
+ month_spacing: 0
66
+ )
67
+ end
68
+
69
+ generate_example("calendar_purple_vibes.svg", "calendar with Purple Vibes palette", force: force) do
70
+ HeatmapBuilder.build_calendar(
71
+ values: calendar_data,
72
+ colors: HeatmapBuilder::Calendar::PURPLE_VIBES,
73
+ cell_size: 14,
74
+ month_spacing: 0
75
+ )
76
+ end
77
+
78
+ generate_example("calendar_red_to_green.svg", "calendar with Red to Green palette", force: force) do
79
+ HeatmapBuilder.build_calendar(
80
+ values: calendar_data,
81
+ colors: HeatmapBuilder::Calendar::RED_TO_GREEN,
82
+ cell_size: 14,
83
+ month_spacing: 0
84
+ )
85
+ end
86
+
87
+ generate_example("calendar_sunday_start.svg", "calendar with Sunday start", force: force) do
88
+ HeatmapBuilder.build_calendar(
89
+ values: calendar_data,
90
+ cell_size: 14,
91
+ start_of_week: :sunday,
92
+ month_spacing: 0
93
+ )
94
+ end
95
+
96
+ generate_example("calendar_with_outside_cells.svg", "calendar with outside cells", force: force) do
97
+ HeatmapBuilder.build_calendar(
98
+ values: calendar_data,
99
+ cell_size: 14,
100
+ show_outside_cells: true,
101
+ month_spacing: 0
102
+ )
103
+ end
104
+
105
+ generate_example("calendar_rounded_corners.svg", "calendar with rounded corners", force: force) do
106
+ HeatmapBuilder.build_calendar(
107
+ values: calendar_data,
108
+ cell_size: 14,
109
+ corner_radius: 2,
110
+ month_spacing: 0
111
+ )
112
+ end
113
+
114
+ generate_example("calendar_rounded_corners_max_radius.svg", "calendar with rounded corners (max radius)", force: force) do
115
+ HeatmapBuilder.build_calendar(
116
+ values: calendar_data,
117
+ cell_size: 14,
118
+ corner_radius: 7,
119
+ month_spacing: 0
120
+ )
121
+ end
122
+
123
+ generate_example("calendar_cell_borders.svg", "calendar with cell borders", force: force) do
124
+ HeatmapBuilder.build_calendar(
125
+ values: calendar_data,
126
+ cell_size: 14,
127
+ border_width: 1,
128
+ border_lightness_factor: 0.7,
129
+ month_spacing: 0
130
+ )
131
+ end
132
+
133
+ generate_example("calendar_no_borders.svg", "calendar with no cell borders", force: force) do
134
+ HeatmapBuilder.build_calendar(
135
+ values: calendar_data,
136
+ cell_size: 14,
137
+ border_width: 1,
138
+ border_lightness_factor: 1,
139
+ month_spacing: 0
140
+ )
141
+ end
142
+
143
+ generate_example("calendar_month_spacing_rounded.svg", "calendar with month spacing and rounded corners", force: force) do
144
+ HeatmapBuilder.build_calendar(
145
+ values: calendar_data,
146
+ cell_size: 14,
147
+ month_spacing: 10,
148
+ corner_radius: 3
149
+ )
150
+ end
151
+
152
+ puts "Done"
153
+ end
154
+
155
+ def sample_calendar_data
156
+ srand(42)
157
+
158
+ current_year = Date.today.year
159
+ start_date = Date.new(current_year, 1, 1)
160
+ end_date = Date.new(current_year, 12, 31)
161
+
162
+ (start_date..end_date).each_with_object({}) do |date, data|
163
+ day_of_year = date.yday
164
+ seasonal_factor = Math.sin((day_of_year - 91.25) * 2 * Math::PI / 365) # Peak in summer
165
+ base_score = (seasonal_factor * 2.7 + 3.6).round
166
+
167
+ score = rand(0..base_score)
168
+ score /= 2 if [0, 6].include?(date.wday) # Weekend reduction
169
+
170
+ data[date.to_s] = score
171
+ end
172
+ end
173
+
174
+ if __FILE__ == $0
175
+ force = ARGV.include?("--force")
176
+ generate_sample_svgs(force: force)
177
+ end