heatmap-builder 0.1.0
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 +7 -0
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +12 -0
- data/.standard.yml +6 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +130 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/calendar_github_style.svg +3 -0
- data/examples/calendar_sunday_start.svg +3 -0
- data/examples/calendar_with_outside_cells.svg +3 -0
- data/examples/generate_samples.rb +114 -0
- data/examples/large_cells.svg +3 -0
- data/examples/weekly_progress.svg +3 -0
- data/heatmap-builder.gemspec +43 -0
- data/lib/heatmap-builder.rb +15 -0
- data/lib/heatmap_builder/calendar_heatmap_builder.rb +295 -0
- data/lib/heatmap_builder/linear_heatmap_builder.rb +128 -0
- data/lib/heatmap_builder/version.rb +3 -0
- data/mise.toml +2 -0
- metadata +128 -0
@@ -0,0 +1,295 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module HeatmapBuilder
|
4
|
+
class CalendarHeatmapBuilder
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
cell_size: 12,
|
7
|
+
cell_spacing: 1,
|
8
|
+
font_size: 8,
|
9
|
+
border_width: 1,
|
10
|
+
colors: %w[#ebedf0 #9be9a8 #40c463 #30a14e #216e39],
|
11
|
+
start_of_week: :monday, # :sunday, :monday, :tuesday, etc.
|
12
|
+
month_spacing: 5, # extra vertical space between months
|
13
|
+
show_month_labels: true,
|
14
|
+
show_day_labels: true,
|
15
|
+
show_outside_cells: false # show cells outside the timeframe with inactive styling
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(scores_by_date, options = {})
|
19
|
+
@scores_by_date = scores_by_date
|
20
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
21
|
+
validate_options!
|
22
|
+
@start_date = parse_date_range.first
|
23
|
+
@end_date = parse_date_range.last
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate
|
27
|
+
build_svg
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :scores_by_date, :options, :start_date, :end_date
|
33
|
+
|
34
|
+
def validate_options!
|
35
|
+
raise Error, "scores_by_date must be a hash" unless scores_by_date.is_a?(Hash)
|
36
|
+
raise Error, "cell_size must be positive" unless options[:cell_size] > 0
|
37
|
+
raise Error, "font_size must be positive" unless options[:font_size] > 0
|
38
|
+
raise Error, "colors must be an array" unless options[:colors].is_a?(Array)
|
39
|
+
raise Error, "must have at least 2 colors" unless options[:colors].length >= 2
|
40
|
+
|
41
|
+
valid_start_days = %i[sunday monday tuesday wednesday thursday friday saturday]
|
42
|
+
unless valid_start_days.include?(options[:start_of_week])
|
43
|
+
raise Error, "start_of_week must be one of: #{valid_start_days.join(", ")}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_date_range
|
48
|
+
dates = scores_by_date.keys.map { |d| d.is_a?(Date) ? d : Date.parse(d.to_s) }
|
49
|
+
return [Date.today - 365, Date.today] if dates.empty?
|
50
|
+
|
51
|
+
[dates.min, dates.max]
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_svg
|
55
|
+
width = svg_width
|
56
|
+
height = svg_height
|
57
|
+
|
58
|
+
svg_content = []
|
59
|
+
|
60
|
+
# Add day labels if enabled
|
61
|
+
if options[:show_day_labels]
|
62
|
+
svg_content << day_labels_svg
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add month labels and cells
|
66
|
+
svg_content << calendar_cells_svg
|
67
|
+
|
68
|
+
if options[:show_month_labels]
|
69
|
+
svg_content << month_labels_svg
|
70
|
+
end
|
71
|
+
|
72
|
+
<<~SVG
|
73
|
+
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
74
|
+
#{svg_content.join}
|
75
|
+
</svg>
|
76
|
+
SVG
|
77
|
+
end
|
78
|
+
|
79
|
+
def calendar_cells_svg
|
80
|
+
svg = ""
|
81
|
+
current_date = calendar_start_date
|
82
|
+
week_index = 0
|
83
|
+
last_month = nil
|
84
|
+
current_x_offset = 0
|
85
|
+
calendar_end_date = calendar_end_date_with_full_weeks
|
86
|
+
|
87
|
+
while current_date <= calendar_end_date
|
88
|
+
# Check if we need to add month spacing
|
89
|
+
if current_date.month != last_month && !last_month.nil?
|
90
|
+
current_x_offset += options[:month_spacing]
|
91
|
+
end
|
92
|
+
last_month = current_date.month
|
93
|
+
|
94
|
+
# Generate week column - always fill all 7 days
|
95
|
+
7.times do |day_index|
|
96
|
+
x = label_offset + week_index * (options[:cell_size] + options[:cell_spacing]) + current_x_offset
|
97
|
+
y = day_label_offset + day_index * (options[:cell_size] + options[:cell_spacing])
|
98
|
+
|
99
|
+
if current_date.between?(start_date, end_date)
|
100
|
+
# Active cell within the specified timeframe
|
101
|
+
score = scores_by_date[current_date] || scores_by_date[current_date.to_s] || 0
|
102
|
+
svg << cell_svg(score, x, y, false)
|
103
|
+
elsif options[:show_outside_cells]
|
104
|
+
# Inactive cell outside the specified timeframe
|
105
|
+
svg << cell_svg(0, x, y, true)
|
106
|
+
end
|
107
|
+
|
108
|
+
current_date += 1
|
109
|
+
end
|
110
|
+
|
111
|
+
week_index += 1
|
112
|
+
end
|
113
|
+
|
114
|
+
svg
|
115
|
+
end
|
116
|
+
|
117
|
+
def cell_svg(score, x, y, inactive = false)
|
118
|
+
color = score_to_color(score)
|
119
|
+
|
120
|
+
# Make inactive cells duller by reducing opacity
|
121
|
+
if inactive
|
122
|
+
color = make_color_inactive(color)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create colored square
|
126
|
+
colored_rect = "<rect x=\"#{x}\" y=\"#{y}\" width=\"#{options[:cell_size]}\" height=\"#{options[:cell_size]}\" fill=\"#{color}\"/>"
|
127
|
+
|
128
|
+
# Create border overlay
|
129
|
+
border_rect = if options[:border_width] > 0
|
130
|
+
inset = options[:border_width] / 2.0
|
131
|
+
border_x = x + inset
|
132
|
+
border_y = y + inset
|
133
|
+
border_size = options[:cell_size] - options[:border_width]
|
134
|
+
border_color = darker_color(color)
|
135
|
+
"<rect x=\"#{border_x}\" y=\"#{border_y}\" width=\"#{border_size}\" height=\"#{border_size}\" fill=\"none\" stroke=\"#{border_color}\" stroke-width=\"#{options[:border_width]}\"/>"
|
136
|
+
else
|
137
|
+
""
|
138
|
+
end
|
139
|
+
|
140
|
+
"#{colored_rect}#{border_rect}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def day_labels_svg
|
144
|
+
return "" unless options[:show_day_labels]
|
145
|
+
|
146
|
+
day_names = day_names_for_week_start
|
147
|
+
svg = ""
|
148
|
+
|
149
|
+
day_names.each_with_index do |day_name, index|
|
150
|
+
y = day_label_offset + index * (options[:cell_size] + options[:cell_spacing]) + options[:cell_size] / 2 + options[:font_size] * 0.35
|
151
|
+
svg << "<text x=\"#{options[:font_size]}\" y=\"#{y}\" text-anchor=\"middle\" font-family=\"Arial, sans-serif\" font-size=\"#{options[:font_size]}\" fill=\"#666666\">#{day_name}</text>"
|
152
|
+
end
|
153
|
+
|
154
|
+
svg
|
155
|
+
end
|
156
|
+
|
157
|
+
def month_labels_svg
|
158
|
+
return "" unless options[:show_month_labels]
|
159
|
+
|
160
|
+
svg = ""
|
161
|
+
current_date = calendar_start_date
|
162
|
+
week_index = 0
|
163
|
+
last_month = nil
|
164
|
+
displayed_months = {}
|
165
|
+
current_x_offset = 0
|
166
|
+
calendar_end_date = calendar_end_date_with_full_weeks
|
167
|
+
|
168
|
+
while current_date <= calendar_end_date
|
169
|
+
# Check if we need to add month spacing
|
170
|
+
if current_date.month != last_month && !last_month.nil?
|
171
|
+
current_x_offset += options[:month_spacing]
|
172
|
+
end
|
173
|
+
|
174
|
+
# Add month label at start of each month, but only if the month overlaps with our specified timeframe
|
175
|
+
if current_date.month != last_month && !displayed_months[current_date.month]
|
176
|
+
# Check if this month has any days within our specified timeframe
|
177
|
+
month_start = Date.new(current_date.year, current_date.month, 1)
|
178
|
+
month_end = Date.new(current_date.year, current_date.month, -1)
|
179
|
+
|
180
|
+
if month_start <= end_date && month_end >= start_date
|
181
|
+
x = label_offset + week_index * (options[:cell_size] + options[:cell_spacing]) + current_x_offset
|
182
|
+
y = options[:font_size] + 2
|
183
|
+
month_name = current_date.strftime("%b")
|
184
|
+
svg << "<text x=\"#{x}\" y=\"#{y}\" font-family=\"Arial, sans-serif\" font-size=\"#{options[:font_size]}\" fill=\"#666666\">#{month_name}</text>"
|
185
|
+
end
|
186
|
+
|
187
|
+
displayed_months[current_date.month] = true
|
188
|
+
end
|
189
|
+
|
190
|
+
last_month = current_date.month
|
191
|
+
current_date += 7
|
192
|
+
week_index += 1
|
193
|
+
end
|
194
|
+
|
195
|
+
svg
|
196
|
+
end
|
197
|
+
|
198
|
+
def score_to_color(score)
|
199
|
+
return options[:colors].first if score == 0
|
200
|
+
|
201
|
+
max_color_index = options[:colors].length - 1
|
202
|
+
color_index = 1 + (score - 1) % max_color_index
|
203
|
+
options[:colors][color_index]
|
204
|
+
end
|
205
|
+
|
206
|
+
def darker_color(hex_color)
|
207
|
+
hex = hex_color.delete("#")
|
208
|
+
r = hex[0..1].to_i(16)
|
209
|
+
g = hex[2..3].to_i(16)
|
210
|
+
b = hex[4..5].to_i(16)
|
211
|
+
|
212
|
+
# Much more subtle border - only 10% darker instead of 30%
|
213
|
+
r = (r * 0.9).to_i
|
214
|
+
g = (g * 0.9).to_i
|
215
|
+
b = (b * 0.9).to_i
|
216
|
+
|
217
|
+
"#%02x%02x%02x" % [r, g, b]
|
218
|
+
end
|
219
|
+
|
220
|
+
def calendar_start_date
|
221
|
+
# Find the start of the week containing start_date
|
222
|
+
days_back = (start_date.wday - week_start_wday) % 7
|
223
|
+
start_date - days_back
|
224
|
+
end
|
225
|
+
|
226
|
+
def calendar_end_date_with_full_weeks
|
227
|
+
# Find the end of the week containing end_date
|
228
|
+
days_forward = (6 - (end_date.wday - week_start_wday)) % 7
|
229
|
+
end_date + days_forward
|
230
|
+
end
|
231
|
+
|
232
|
+
def make_color_inactive(hex_color)
|
233
|
+
# Make color duller by reducing saturation and increasing lightness
|
234
|
+
hex = hex_color.delete("#")
|
235
|
+
r = hex[0..1].to_i(16)
|
236
|
+
g = hex[2..3].to_i(16)
|
237
|
+
b = hex[4..5].to_i(16)
|
238
|
+
|
239
|
+
# Blend with light gray to make it appear duller/inactive
|
240
|
+
gray = 230
|
241
|
+
mix_ratio = 0.6 # 60% original color, 40% gray
|
242
|
+
|
243
|
+
r = (r * mix_ratio + gray * (1 - mix_ratio)).to_i
|
244
|
+
g = (g * mix_ratio + gray * (1 - mix_ratio)).to_i
|
245
|
+
b = (b * mix_ratio + gray * (1 - mix_ratio)).to_i
|
246
|
+
|
247
|
+
"#%02x%02x%02x" % [r, g, b]
|
248
|
+
end
|
249
|
+
|
250
|
+
def week_start_wday
|
251
|
+
case options[:start_of_week]
|
252
|
+
when :sunday then 0
|
253
|
+
when :monday then 1
|
254
|
+
when :tuesday then 2
|
255
|
+
when :wednesday then 3
|
256
|
+
when :thursday then 4
|
257
|
+
when :friday then 5
|
258
|
+
when :saturday then 6
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def day_names_for_week_start
|
263
|
+
all_days = %w[S M T W T F S]
|
264
|
+
start_index = week_start_wday
|
265
|
+
all_days.rotate(start_index)
|
266
|
+
end
|
267
|
+
|
268
|
+
def month_spacing_weeks
|
269
|
+
options[:month_spacing] / (options[:cell_size] + options[:cell_spacing])
|
270
|
+
end
|
271
|
+
|
272
|
+
def label_offset
|
273
|
+
options[:show_day_labels] ? options[:font_size] * 2 : 0
|
274
|
+
end
|
275
|
+
|
276
|
+
def day_label_offset
|
277
|
+
options[:show_month_labels] ? options[:font_size] + 5 : 0
|
278
|
+
end
|
279
|
+
|
280
|
+
def svg_width
|
281
|
+
weeks_count = ((calendar_end_date_with_full_weeks - calendar_start_date) / 7).ceil
|
282
|
+
month_spacing_total = (months_in_range - 1) * options[:month_spacing]
|
283
|
+
|
284
|
+
label_offset + weeks_count * (options[:cell_size] + options[:cell_spacing]) + month_spacing_total
|
285
|
+
end
|
286
|
+
|
287
|
+
def svg_height
|
288
|
+
day_label_offset + 7 * (options[:cell_size] + options[:cell_spacing])
|
289
|
+
end
|
290
|
+
|
291
|
+
def months_in_range
|
292
|
+
((end_date.year - start_date.year) * 12 + end_date.month - start_date.month + 1)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module HeatmapBuilder
|
2
|
+
class LinearHeatmapBuilder
|
3
|
+
DEFAULT_OPTIONS = {
|
4
|
+
cell_size: 10,
|
5
|
+
cell_spacing: 1,
|
6
|
+
font_size: 8,
|
7
|
+
cells_per_row: 7,
|
8
|
+
border_width: 1,
|
9
|
+
colors: %w[#ebedf0 #9be9a8 #40c463 #30a14e #216e39]
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def initialize(scores, options = {})
|
13
|
+
@scores = scores
|
14
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
15
|
+
validate_options!
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate
|
19
|
+
build_svg
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :scores, :options
|
25
|
+
|
26
|
+
def validate_options!
|
27
|
+
raise Error, "scores must be an array" unless scores.is_a?(Array)
|
28
|
+
raise Error, "cell_size must be positive" unless options[:cell_size] > 0
|
29
|
+
raise Error, "font_size must be positive" unless options[:font_size] > 0
|
30
|
+
raise Error, "cells_per_row must be positive" unless options[:cells_per_row] > 0
|
31
|
+
raise Error, "colors must be an array" unless options[:colors].is_a?(Array)
|
32
|
+
raise Error, "must have at least 2 colors" unless options[:colors].length >= 2
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_svg
|
36
|
+
width = svg_width
|
37
|
+
height = svg_height
|
38
|
+
|
39
|
+
svg_content = scores.first(options[:cells_per_row]).map.with_index do |score, index|
|
40
|
+
cell_svg(score, index)
|
41
|
+
end.join
|
42
|
+
|
43
|
+
<<~SVG
|
44
|
+
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
45
|
+
#{svg_content}
|
46
|
+
</svg>
|
47
|
+
SVG
|
48
|
+
end
|
49
|
+
|
50
|
+
def cell_svg(score, index)
|
51
|
+
# Calculate x position - each cell takes cell_size + spacing
|
52
|
+
x = index * (options[:cell_size] + options[:cell_spacing])
|
53
|
+
y = 0
|
54
|
+
|
55
|
+
color = score_to_color(score)
|
56
|
+
|
57
|
+
# Create colored square (full cell size)
|
58
|
+
colored_rect = "<rect x=\"#{x}\" y=\"#{y}\" width=\"#{options[:cell_size]}\" height=\"#{options[:cell_size]}\" fill=\"#{color}\"/>"
|
59
|
+
|
60
|
+
# Create border overlay completely inside the colored square
|
61
|
+
border_rect = if options[:border_width] > 0
|
62
|
+
# Inset the border rect by half the stroke width so stroke stays inside
|
63
|
+
inset = options[:border_width] / 2.0
|
64
|
+
border_x = x + inset
|
65
|
+
border_y = y + inset
|
66
|
+
border_size = options[:cell_size] - options[:border_width]
|
67
|
+
border_color = darker_color(color)
|
68
|
+
"<rect x=\"#{border_x}\" y=\"#{border_y}\" width=\"#{border_size}\" height=\"#{border_size}\" fill=\"none\" stroke=\"#{border_color}\" stroke-width=\"#{options[:border_width]}\"/>"
|
69
|
+
else
|
70
|
+
""
|
71
|
+
end
|
72
|
+
|
73
|
+
# Calculate text position (center of cell)
|
74
|
+
text_x = x + options[:cell_size] / 2
|
75
|
+
# For better vertical centering: cell center + font_size * 0.35 (accounts for baseline)
|
76
|
+
text_y = y + options[:cell_size] / 2 + options[:font_size] * 0.35
|
77
|
+
|
78
|
+
text_element = "<text x=\"#{text_x}\" y=\"#{text_y}\" text-anchor=\"middle\" font-family=\"Arial, sans-serif\" font-size=\"#{options[:font_size]}\" fill=\"#{text_color(color)}\">#{score}</text>"
|
79
|
+
|
80
|
+
"#{colored_rect}#{border_rect}#{text_element}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def score_to_color(score)
|
84
|
+
return options[:colors].first if score == 0
|
85
|
+
|
86
|
+
max_color_index = options[:colors].length - 1
|
87
|
+
# Map score to color index, ensuring we don't exceed available colors
|
88
|
+
color_index = 1 + (score - 1) % max_color_index
|
89
|
+
options[:colors][color_index]
|
90
|
+
end
|
91
|
+
|
92
|
+
def text_color(background_color)
|
93
|
+
# Simple contrast check - if dark background, use white text
|
94
|
+
hex = background_color.delete("#")
|
95
|
+
r = hex[0..1].to_i(16)
|
96
|
+
g = hex[2..3].to_i(16)
|
97
|
+
b = hex[4..5].to_i(16)
|
98
|
+
brightness = (r * 299 + g * 587 + b * 114) / 1000
|
99
|
+
(brightness > 128) ? "#000000" : "#ffffff"
|
100
|
+
end
|
101
|
+
|
102
|
+
def svg_width
|
103
|
+
options[:cells_per_row] * options[:cell_size] +
|
104
|
+
(options[:cells_per_row] - 1) * options[:cell_spacing]
|
105
|
+
end
|
106
|
+
|
107
|
+
def svg_height
|
108
|
+
options[:cell_size]
|
109
|
+
end
|
110
|
+
|
111
|
+
def darker_color(hex_color)
|
112
|
+
# Remove # if present
|
113
|
+
hex = hex_color.delete("#")
|
114
|
+
|
115
|
+
# Extract RGB values
|
116
|
+
r = hex[0..1].to_i(16)
|
117
|
+
g = hex[2..3].to_i(16)
|
118
|
+
b = hex[4..5].to_i(16)
|
119
|
+
|
120
|
+
# Make 30% darker
|
121
|
+
r = (r * 0.7).to_i
|
122
|
+
g = (g * 0.7).to_i
|
123
|
+
b = (b * 0.7).to_i
|
124
|
+
|
125
|
+
"#%02x%02x%02x" % [r, g, b]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/mise.toml
ADDED
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heatmap-builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Musayev
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: standard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
description: A Ruby gem that generates embeddable GitHub-style heatmap visualization
|
70
|
+
in SVG format.
|
71
|
+
email:
|
72
|
+
- alex.musayev@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".github/workflows/ci.yml"
|
78
|
+
- ".gitignore"
|
79
|
+
- ".standard.yml"
|
80
|
+
- CHANGELOG.md
|
81
|
+
- CODE_OF_CONDUCT.md
|
82
|
+
- Gemfile
|
83
|
+
- Gemfile.lock
|
84
|
+
- LICENSE.txt
|
85
|
+
- README.md
|
86
|
+
- Rakefile
|
87
|
+
- bin/console
|
88
|
+
- bin/setup
|
89
|
+
- examples/calendar_github_style.svg
|
90
|
+
- examples/calendar_sunday_start.svg
|
91
|
+
- examples/calendar_with_outside_cells.svg
|
92
|
+
- examples/generate_samples.rb
|
93
|
+
- examples/large_cells.svg
|
94
|
+
- examples/weekly_progress.svg
|
95
|
+
- heatmap-builder.gemspec
|
96
|
+
- lib/heatmap-builder.rb
|
97
|
+
- lib/heatmap_builder/calendar_heatmap_builder.rb
|
98
|
+
- lib/heatmap_builder/linear_heatmap_builder.rb
|
99
|
+
- lib/heatmap_builder/version.rb
|
100
|
+
- mise.toml
|
101
|
+
homepage: https://github.com/dreikanter/heatmap-builder
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata:
|
105
|
+
allowed_push_host: https://rubygems.org
|
106
|
+
homepage_uri: https://github.com/dreikanter/heatmap-builder
|
107
|
+
source_code_uri: https://github.com/dreikanter/heatmap-builder.git
|
108
|
+
changelog_uri: https://github.com/dreikanter/heatmap-builder/blob/main/CHANGELOG.md
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.0'
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubygems_version: 3.5.22
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Generate SVG heatmap visualizations
|
128
|
+
test_files: []
|