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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -5
- data/.gitignore +3 -0
- data/CHANGELOG.md +101 -3
- data/Gemfile.lock +35 -24
- data/README.md +325 -52
- data/Rakefile +15 -0
- data/bin/generate_examples +177 -0
- data/examples/calendar_blue_ocean.svg +1 -0
- data/examples/calendar_cell_borders.svg +1 -0
- data/examples/calendar_default.svg +1 -0
- data/examples/calendar_github_style.svg +1 -3
- data/examples/calendar_month_spacing_rounded.svg +1 -0
- data/examples/calendar_no_borders.svg +1 -0
- data/examples/calendar_purple_vibes.svg +1 -0
- data/examples/calendar_red_to_green.svg +1 -0
- data/examples/calendar_rounded_corners.svg +1 -0
- data/examples/calendar_rounded_corners_max_radius.svg +1 -0
- data/examples/calendar_sunday_start.svg +1 -3
- data/examples/calendar_warm_sunset.svg +1 -0
- data/examples/calendar_with_outside_cells.svg +1 -3
- data/heatmap-builder.gemspec +5 -4
- data/lib/heatmap-builder.rb +24 -6
- data/lib/heatmap_builder/calendar.rb +426 -0
- data/lib/heatmap_builder/color_helpers.rb +150 -0
- data/lib/heatmap_builder/svg_helpers.rb +82 -0
- data/lib/heatmap_builder/value_conversion.rb +68 -0
- data/lib/heatmap_builder/version.rb +1 -1
- metadata +38 -15
- data/examples/generate_samples.rb +0 -114
- data/examples/large_cells.svg +0 -3
- data/examples/weekly_progress.svg +0 -3
- data/lib/heatmap_builder/calendar_heatmap_builder.rb +0 -295
- data/lib/heatmap_builder/linear_heatmap_builder.rb +0 -128
- data/mise.toml +0 -2
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
|
|
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
|

|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- GitHub-style calendar layouts for date-based data.
|
|
10
|
-
-
|
|
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
|
-
-
|
|
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
|
-
###
|
|
39
|
+
### Calendar Heatmaps
|
|
35
40
|
|
|
36
41
|
```ruby
|
|
37
|
-
|
|
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
|
+

|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
115
|
+
svg = HeatmapBuilder.build_calendar(
|
|
116
|
+
values: values_by_date,
|
|
117
|
+
value_min: 0,
|
|
118
|
+
value_max: 100
|
|
119
|
+
)
|
|
45
120
|
```
|
|
46
121
|
|
|
47
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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
|
+

|
|
169
|
+
|
|
170
|
+
#### Blue Ocean
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::BLUE_OCEAN)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+

|
|
177
|
+
|
|
178
|
+
#### Warm Sunset
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::WARM_SUNSET)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+

|
|
185
|
+
|
|
186
|
+
#### Purple Vibes
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
HeatmapBuilder.build_calendar(scores: calendar_data, colors: HeatmapBuilder::Calendar::PURPLE_VIBES)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+

|
|
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
|
-
|
|
200
|
+

|
|
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
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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
|
+

|
|
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
|
-

|
|
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
|
|
87
|
-
|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
287
|
+
month_spacing: 10,
|
|
288
|
+
corner_radius: 3
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+

|
|
92
293
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
372
|
+
After checking out the repo, run `bin/setup` to install development dependencies.
|
|
373
|
+
|
|
374
|
+
### Running Tests
|
|
107
375
|
|
|
108
376
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
377
|
+
# Run all tests
|
|
378
|
+
rake test
|
|
379
|
+
|
|
380
|
+
# Run tests with code linting
|
|
381
|
+
rake
|
|
111
382
|
|
|
112
|
-
|
|
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
|
-
|
|
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/
|
|
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
|