heatmap-builder 0.1.0 → 0.2.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.gitignore +3 -0
  4. data/CHANGELOG.md +33 -2
  5. data/Gemfile.lock +9 -1
  6. data/README.md +340 -37
  7. data/Rakefile +15 -0
  8. data/bin/generate_examples +234 -0
  9. data/examples/calendar_blue_ocean.svg +1 -0
  10. data/examples/calendar_default.svg +1 -0
  11. data/examples/calendar_github_style.svg +1 -3
  12. data/examples/calendar_purple_vibes.svg +1 -0
  13. data/examples/calendar_red_to_green.svg +1 -0
  14. data/examples/calendar_rounded_corners.svg +1 -0
  15. data/examples/calendar_rounded_corners_max_radius.svg +1 -0
  16. data/examples/calendar_sunday_start.svg +1 -3
  17. data/examples/calendar_warm_sunset.svg +1 -0
  18. data/examples/calendar_with_outside_cells.svg +1 -3
  19. data/examples/large_cells.svg +1 -3
  20. data/examples/linear_blue_ocean.svg +1 -0
  21. data/examples/linear_github_green.svg +1 -0
  22. data/examples/linear_neon_gradient.svg +1 -0
  23. data/examples/linear_purple_vibes.svg +1 -0
  24. data/examples/linear_red_to_green.svg +1 -0
  25. data/examples/linear_rounded_corners.svg +1 -0
  26. data/examples/linear_rounded_corners_max_radius.svg +1 -0
  27. data/examples/linear_warm_sunset.svg +1 -0
  28. data/examples/weekly_progress.svg +1 -3
  29. data/heatmap-builder.gemspec +1 -0
  30. data/lib/heatmap-builder.rb +47 -3
  31. data/lib/heatmap_builder/builder.rb +100 -0
  32. data/lib/heatmap_builder/calendar_heatmap_builder.rb +177 -162
  33. data/lib/heatmap_builder/color_helpers.rb +170 -0
  34. data/lib/heatmap_builder/linear_heatmap_builder.rb +49 -103
  35. data/lib/heatmap_builder/svg_helpers.rb +75 -0
  36. data/lib/heatmap_builder/value_conversion.rb +64 -0
  37. data/lib/heatmap_builder/version.rb +1 -1
  38. metadata +36 -4
  39. data/examples/generate_samples.rb +0 -114
  40. data/mise.toml +0 -2
@@ -0,0 +1,170 @@
1
+ module HeatmapBuilder
2
+ module ColorHelpers
3
+ private
4
+
5
+ def score_to_color(score, colors:)
6
+ # Generate color palette if colors is a hash
7
+ if colors.is_a?(Hash)
8
+ colors = generate_color_palette(colors[:from], colors[:to], colors[:steps])
9
+ end
10
+
11
+ return colors.first if score == 0
12
+
13
+ max_color_index = colors.length - 1
14
+ color_index = 1 + (score - 1) % max_color_index
15
+ colors[color_index]
16
+ end
17
+
18
+ def darker_color(hex_color, factor: 0.9)
19
+ rgb = hex_to_rgb(hex_color)
20
+ oklch = rgb_to_oklch(*rgb)
21
+
22
+ # Reduce lightness (L component) by factor
23
+ darker_oklch = [oklch[0] * factor, oklch[1], oklch[2]]
24
+ darker_rgb = oklch_to_rgb(*darker_oklch)
25
+
26
+ rgb_to_hex(*darker_rgb)
27
+ end
28
+
29
+ def make_color_inactive(hex_color)
30
+ # Convert to OKLCH for blending
31
+ rgb = hex_to_rgb(hex_color)
32
+ oklch = rgb_to_oklch(*rgb)
33
+
34
+ # Reduce chroma (saturation) to make it more muted
35
+ # Also slightly reduce lightness
36
+ inactive_oklch = [
37
+ oklch[0] * 0.85, # Slightly reduce lightness
38
+ oklch[1] * 0.4, # Significantly reduce chroma (saturation)
39
+ oklch[2] # Keep hue unchanged
40
+ ]
41
+
42
+ inactive_rgb = oklch_to_rgb(*inactive_oklch)
43
+ rgb_to_hex(*inactive_rgb)
44
+ end
45
+
46
+ def rgb_to_oklch(r, g, b)
47
+ # Convert to linear RGB first
48
+ r_linear = srgb_to_linear(r / 255.0)
49
+ g_linear = srgb_to_linear(g / 255.0)
50
+ b_linear = srgb_to_linear(b / 255.0)
51
+
52
+ # Linear RGB to OKLab using the Oklab transformation matrix
53
+ l = 0.4122214708 * r_linear + 0.5363325363 * g_linear + 0.0514459929 * b_linear
54
+ m = 0.2119034982 * r_linear + 0.6806995451 * g_linear + 0.1073969566 * b_linear
55
+ s = 0.0883024619 * r_linear + 0.2817188376 * g_linear + 0.6299787005 * b_linear
56
+
57
+ # Apply cube root
58
+ l_root = (l >= 0) ? l**(1.0 / 3) : -((-l)**(1.0 / 3))
59
+ m_root = (m >= 0) ? m**(1.0 / 3) : -((-m)**(1.0 / 3))
60
+ s_root = (s >= 0) ? s**(1.0 / 3) : -((-s)**(1.0 / 3))
61
+
62
+ # Convert to OKLab
63
+ ok_l = 0.2104542553 * l_root + 0.7936177850 * m_root - 0.0040720468 * s_root
64
+ ok_a = 1.9779984951 * l_root - 2.4285922050 * m_root + 0.4505937099 * s_root
65
+ ok_b = 0.0259040371 * l_root + 0.7827717662 * m_root - 0.8086757660 * s_root
66
+
67
+ # Convert OKLab to OKLCH
68
+ chroma = Math.sqrt(ok_a * ok_a + ok_b * ok_b)
69
+ hue = Math.atan2(ok_b, ok_a) * 180.0 / Math::PI
70
+ hue += 360 if hue < 0
71
+
72
+ [ok_l, chroma, hue]
73
+ end
74
+
75
+ def oklch_to_rgb(ok_l, chroma, hue)
76
+ # Convert OKLCH to OKLab
77
+ hue_rad = hue * Math::PI / 180.0
78
+ ok_a = chroma * Math.cos(hue_rad)
79
+ ok_b = chroma * Math.sin(hue_rad)
80
+
81
+ # OKLab to linear RGB
82
+ l_root = ok_l + 0.3963377774 * ok_a + 0.2158037573 * ok_b
83
+ m_root = ok_l - 0.1055613458 * ok_a - 0.0638541728 * ok_b
84
+ s_root = ok_l - 0.0894841775 * ok_a - 1.2914855480 * ok_b
85
+
86
+ # Cube the values
87
+ l = l_root * l_root * l_root
88
+ m = m_root * m_root * m_root
89
+ s = s_root * s_root * s_root
90
+
91
+ # Convert to linear RGB
92
+ r_linear = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
93
+ g_linear = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
94
+ b_linear = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
95
+
96
+ # Convert to sRGB
97
+ r = linear_to_srgb(r_linear)
98
+ g = linear_to_srgb(g_linear)
99
+ b = linear_to_srgb(b_linear)
100
+
101
+ # Clamp to 0-255 and convert to integers
102
+ r = (r * 255).clamp(0, 255).round
103
+ g = (g * 255).clamp(0, 255).round
104
+ b = (b * 255).clamp(0, 255).round
105
+
106
+ [r, g, b]
107
+ end
108
+
109
+ def srgb_to_linear(component)
110
+ (component <= 0.04045) ? component / 12.92 : ((component + 0.055) / 1.055)**2.4
111
+ end
112
+
113
+ def linear_to_srgb(component)
114
+ (component <= 0.0031308) ? component * 12.92 : 1.055 * (component**(1.0 / 2.4)) - 0.055
115
+ end
116
+
117
+ def interpolate_oklch(oklch1, oklch2, ratio)
118
+ # Handle hue interpolation (shortest path around the circle)
119
+ hue1, hue2 = oklch1[2], oklch2[2]
120
+ hue_diff = hue2 - hue1
121
+
122
+ # Take shorter path around the circle
123
+ if hue_diff > 180
124
+ hue_diff -= 360
125
+ elsif hue_diff < -180
126
+ hue_diff += 360
127
+ end
128
+
129
+ interpolated_hue = hue1 + hue_diff * ratio
130
+ interpolated_hue += 360 if interpolated_hue < 0
131
+ interpolated_hue -= 360 if interpolated_hue >= 360
132
+
133
+ [
134
+ oklch1[0] + (oklch2[0] - oklch1[0]) * ratio, # L (lightness)
135
+ oklch1[1] + (oklch2[1] - oklch1[1]) * ratio, # C (chroma)
136
+ interpolated_hue # H (hue)
137
+ ]
138
+ end
139
+
140
+ def hex_to_rgb(hex_color)
141
+ hex = hex_color.delete("#")
142
+ r = hex[0..1].to_i(16)
143
+ g = hex[2..3].to_i(16)
144
+ b = hex[4..5].to_i(16)
145
+ [r, g, b]
146
+ end
147
+
148
+ def rgb_to_hex(r, g, b)
149
+ "#%02x%02x%02x" % [r, g, b]
150
+ end
151
+
152
+ def generate_color_palette(from_color, to_color, steps)
153
+ from_rgb = hex_to_rgb(from_color)
154
+ to_rgb = hex_to_rgb(to_color)
155
+
156
+ from_oklch = rgb_to_oklch(*from_rgb)
157
+ to_oklch = rgb_to_oklch(*to_rgb)
158
+
159
+ colors = []
160
+ (0...steps).each do |i|
161
+ ratio = i.to_f / (steps - 1)
162
+ interpolated_oklch = interpolate_oklch(from_oklch, to_oklch, ratio)
163
+ interpolated_rgb = oklch_to_rgb(*interpolated_oklch)
164
+ colors << rgb_to_hex(*interpolated_rgb)
165
+ end
166
+
167
+ colors
168
+ end
169
+ end
170
+ end
@@ -1,128 +1,74 @@
1
+ require_relative "builder"
2
+ require_relative "value_conversion"
3
+
1
4
  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
5
+ class LinearHeatmapBuilder < Builder
6
+ include ValueConversion
17
7
 
18
- def generate
19
- build_svg
8
+ def build
9
+ svg_content = computed_scores.map.with_index do |score, index|
10
+ cell_svg(score, index)
11
+ end.join
12
+
13
+ svg_container(
14
+ width: computed_scores.length * options[:cell_size] + (computed_scores.length - 1) * options[:cell_spacing],
15
+ height: options[:cell_size]
16
+ ) { svg_content }
20
17
  end
21
18
 
22
19
  private
23
20
 
24
- attr_reader :scores, :options
25
-
26
21
  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
22
+ super
23
+
24
+ raise Error, "scores must be an array" if scores && !scores.is_a?(Array)
25
+ raise Error, "values must be an array" if values && !values.is_a?(Array)
33
26
  end
34
27
 
35
- def build_svg
36
- width = svg_width
37
- height = svg_height
28
+ def computed_scores
29
+ @computed_scores ||= scores || values.map.with_index { |value, index| convert_value_to_score(value, index: index) }
30
+ end
38
31
 
39
- svg_content = scores.first(options[:cells_per_row]).map.with_index do |score, index|
40
- cell_svg(score, index)
41
- end.join
32
+ def calculated_min_from_values
33
+ non_nil_values = values.compact
34
+ non_nil_values.empty? ? 0 : non_nil_values.min
35
+ end
42
36
 
43
- <<~SVG
44
- <svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
45
- #{svg_content}
46
- </svg>
47
- SVG
37
+ def calculated_max_from_values
38
+ non_nil_values = values.compact
39
+ non_nil_values.empty? ? 0 : non_nil_values.max
48
40
  end
49
41
 
50
42
  def cell_svg(score, index)
51
- # Calculate x position - each cell takes cell_size + spacing
52
43
  x = index * (options[:cell_size] + options[:cell_spacing])
53
44
  y = 0
54
45
 
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>"
46
+ color = score_to_color(score, colors: color_palette)
79
47
 
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
48
+ colored_rect = svg_rect(
49
+ x: x, y: y,
50
+ width: options[:cell_size], height: options[:cell_size],
51
+ rx: options[:corner_radius],
52
+ fill: color
53
+ )
101
54
 
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("#")
55
+ border_rect = cell_border(
56
+ x, y, color,
57
+ cell_size: options[:cell_size],
58
+ border_width: options[:border_width],
59
+ corner_radius: options[:corner_radius]
60
+ )
114
61
 
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)
62
+ text_x = x + options[:cell_size] / 2
63
+ text_y = y + options[:cell_size] / 2 + options[:font_size] * 0.35
119
64
 
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
65
+ text_element = svg_text(
66
+ score,
67
+ x: text_x, y: text_y,
68
+ font_size: options[:font_size], fill: options[:text_color]
69
+ )
124
70
 
125
- "#%02x%02x%02x" % [r, g, b]
71
+ "#{colored_rect}#{border_rect}#{text_element}"
126
72
  end
127
73
  end
128
74
  end
@@ -0,0 +1,75 @@
1
+ module HeatmapBuilder
2
+ module SvgHelpers
3
+ private
4
+
5
+ def svg_element(tag, attributes = {}, &block)
6
+ attr_string = attributes.map do |key, value|
7
+ "#{kebab_case(key)}=\"#{value}\""
8
+ end.join(" ")
9
+
10
+ attr_string = " #{attr_string}" unless attr_string.empty?
11
+
12
+ if block_given?
13
+ content = block.call
14
+ "<#{tag}#{attr_string}>#{content}</#{tag}>"
15
+ else
16
+ "<#{tag}#{attr_string}/>"
17
+ end
18
+ end
19
+
20
+ def svg_rect(x:, y:, width:, height:, rx: nil, **attributes)
21
+ attrs = {
22
+ x: x,
23
+ y: y,
24
+ width: width,
25
+ height: height
26
+ }
27
+
28
+ attrs[:rx] = rx if rx && rx > 0
29
+ svg_element("rect", attrs.merge(attributes))
30
+ end
31
+
32
+ def svg_text(content, x:, y:, **attributes)
33
+ default_attrs = {
34
+ text_anchor: "middle",
35
+ font_family: "Arial, sans-serif"
36
+ }
37
+
38
+ svg_element("text", {x: x, y: y}.merge(default_attrs).merge(attributes)) { content }
39
+ end
40
+
41
+ def svg_container(width:, height:, &block)
42
+ svg_element("svg", {
43
+ width: width,
44
+ height: height,
45
+ xmlns: "http://www.w3.org/2000/svg"
46
+ }, &block)
47
+ end
48
+
49
+ def kebab_case(key)
50
+ key.to_s.tr("_", "-")
51
+ end
52
+
53
+ def cell_border(x, y, color, cell_size:, border_width:, corner_radius:)
54
+ return "" unless border_width > 0
55
+
56
+ inset = border_width / 2.0
57
+ border_x = x + inset
58
+ border_y = y + inset
59
+ border_size = cell_size - border_width
60
+ border_color = darker_color(color)
61
+ border_radius = (corner_radius > 0) ? [corner_radius - inset, 0].max : 0
62
+
63
+ svg_rect(
64
+ x: border_x,
65
+ y: border_y,
66
+ width: border_size,
67
+ height: border_size,
68
+ rx: border_radius,
69
+ fill: "none",
70
+ stroke: border_color,
71
+ stroke_width: border_width
72
+ )
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,64 @@
1
+ module HeatmapBuilder
2
+ module ValueConversion
3
+ private
4
+
5
+ def value_min
6
+ @value_min ||= options[:value_min] || calculated_min_from_values
7
+ end
8
+
9
+ def value_max
10
+ @value_max ||= options[:value_max] || calculated_max_from_values
11
+ end
12
+
13
+ def calculated_min_from_values
14
+ raise NotImplementedError, "Subclasses must implement #calculated_min_from_values"
15
+ end
16
+
17
+ def calculated_max_from_values
18
+ raise NotImplementedError, "Subclasses must implement #calculated_max_from_values"
19
+ end
20
+
21
+ def color_count
22
+ @color_count ||= begin
23
+ colors_option = options[:colors]
24
+ if colors_option.is_a?(Array)
25
+ colors_option.length
26
+ elsif colors_option.is_a?(Hash)
27
+ colors_option[:steps]
28
+ else
29
+ raise Error, "colors must be an array or hash"
30
+ end
31
+ end
32
+ end
33
+
34
+ def convert_value_to_score(value, **params)
35
+ value = value_min if value.nil?
36
+
37
+ if options[:value_to_score]
38
+ score = options[:value_to_score].call(
39
+ value: value,
40
+ min: value_min,
41
+ max: value_max,
42
+ max_score: color_count - 1,
43
+ **params
44
+ )
45
+
46
+ unless score.is_a?(Integer) && score >= 0 && score < color_count
47
+ raise Error, "value_to_score must return an integer between 0 and #{color_count - 1}, got #{score.inspect}"
48
+ end
49
+
50
+ return score
51
+ end
52
+
53
+ clamped_value = value.clamp(value_min, value_max)
54
+
55
+ if value_min == value_max
56
+ 0
57
+ else
58
+ range = value_max - value_min
59
+ normalized = (clamped_value - value_min).to_f / range
60
+ (normalized * (color_count - 1)).round
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,3 @@
1
1
  module HeatmapBuilder
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heatmap-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Musayev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-19 00:00:00.000000000 Z
11
+ date: 2025-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.22'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.22'
69
83
  description: A Ruby gem that generates embeddable GitHub-style heatmap visualization
70
84
  in SVG format.
71
85
  email:
@@ -85,19 +99,37 @@ files:
85
99
  - README.md
86
100
  - Rakefile
87
101
  - bin/console
102
+ - bin/generate_examples
88
103
  - bin/setup
104
+ - examples/calendar_blue_ocean.svg
105
+ - examples/calendar_default.svg
89
106
  - examples/calendar_github_style.svg
107
+ - examples/calendar_purple_vibes.svg
108
+ - examples/calendar_red_to_green.svg
109
+ - examples/calendar_rounded_corners.svg
110
+ - examples/calendar_rounded_corners_max_radius.svg
90
111
  - examples/calendar_sunday_start.svg
112
+ - examples/calendar_warm_sunset.svg
91
113
  - examples/calendar_with_outside_cells.svg
92
- - examples/generate_samples.rb
93
114
  - examples/large_cells.svg
115
+ - examples/linear_blue_ocean.svg
116
+ - examples/linear_github_green.svg
117
+ - examples/linear_neon_gradient.svg
118
+ - examples/linear_purple_vibes.svg
119
+ - examples/linear_red_to_green.svg
120
+ - examples/linear_rounded_corners.svg
121
+ - examples/linear_rounded_corners_max_radius.svg
122
+ - examples/linear_warm_sunset.svg
94
123
  - examples/weekly_progress.svg
95
124
  - heatmap-builder.gemspec
96
125
  - lib/heatmap-builder.rb
126
+ - lib/heatmap_builder/builder.rb
97
127
  - lib/heatmap_builder/calendar_heatmap_builder.rb
128
+ - lib/heatmap_builder/color_helpers.rb
98
129
  - lib/heatmap_builder/linear_heatmap_builder.rb
130
+ - lib/heatmap_builder/svg_helpers.rb
131
+ - lib/heatmap_builder/value_conversion.rb
99
132
  - lib/heatmap_builder/version.rb
100
- - mise.toml
101
133
  homepage: https://github.com/dreikanter/heatmap-builder
102
134
  licenses:
103
135
  - MIT
@@ -1,114 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "date"
4
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
- require "heatmap-builder"
6
-
7
- def generate_sample_svgs
8
- # Create examples directory for SVG files
9
- Dir.mkdir("examples") unless Dir.exist?("examples")
10
-
11
- generated_files = []
12
-
13
- # Basic weekly progress
14
- puts "Generating basic weekly progress..."
15
- weekly_scores = [0, 1, 3, 2, 4, 1, 0]
16
- svg = HeatmapBuilder.generate(weekly_scores, cell_size: 18)
17
- filepath = "examples/weekly_progress.svg"
18
- File.write(filepath, svg)
19
- generated_files << filepath
20
-
21
- # Large cells example
22
- puts "Generating large cells example..."
23
- large_cell_scores = [1, 2, 3, 4, 5, 6, 7]
24
- svg = HeatmapBuilder.generate(large_cell_scores, {
25
- cell_size: 35,
26
- cell_spacing: 1,
27
- font_size: 20
28
- })
29
- filepath = "examples/large_cells.svg"
30
- File.write(filepath, svg)
31
- generated_files << filepath
32
-
33
- # GitHub-style calendar
34
- puts "Generating GitHub-style calendar..."
35
- calendar_data = sample_calendar_data
36
- svg = HeatmapBuilder.generate_calendar(calendar_data, {
37
- cell_size: 14,
38
- month_spacing: 0
39
- })
40
- filepath = "examples/calendar_github_style.svg"
41
- File.write(filepath, svg)
42
- generated_files << filepath
43
-
44
- # Calendar with Sunday start
45
- puts "Generating calendar with Sunday start..."
46
- svg = HeatmapBuilder.generate_calendar(calendar_data, {
47
- cell_size: 14,
48
- start_of_week: :sunday,
49
- month_spacing: 0
50
- })
51
- filepath = "examples/calendar_sunday_start.svg"
52
- File.write(filepath, svg)
53
- generated_files << filepath
54
-
55
- # Calendar with outside cells
56
- puts "Generating calendar with outside cells..."
57
- svg = HeatmapBuilder.generate_calendar(calendar_data, {
58
- cell_size: 14,
59
- show_outside_cells: true,
60
- month_spacing: 0
61
- })
62
- filepath = "examples/calendar_with_outside_cells.svg"
63
- File.write(filepath, svg)
64
- generated_files << filepath
65
-
66
- puts "\nāœ… Sample SVG files generated successfully!"
67
- puts "šŸ“ Generated files:"
68
- generated_files.each do |file|
69
- puts " - #{file}"
70
- end
71
- puts "šŸ“‚ Total files: #{generated_files.length}"
72
- end
73
-
74
- def sample_calendar_data
75
- # Generate sample data for a full year
76
- end_date = Date.today
77
- start_date = Date.new(end_date.year, 1, 1)
78
-
79
- data = {}
80
- current_date = start_date
81
-
82
- while current_date <= end_date
83
- # Create some realistic activity patterns with seasonal variation
84
- base_activity = case current_date.month
85
- when 1, 2, 12 # Winter - lower activity
86
- [0, 0, 0, 1, 1, 2]
87
- when 3, 4, 5 # Spring - increasing activity
88
- [0, 1, 1, 2, 2, 3, 3]
89
- when 6, 7, 8 # Summer - peak activity
90
- [1, 2, 2, 3, 3, 4, 4, 5]
91
- when 9, 10, 11 # Fall - moderate activity
92
- [0, 1, 2, 2, 3, 3]
93
- end
94
-
95
- # Weekend vs weekday patterns
96
- score = case current_date.wday
97
- when 0, 6 # Weekend - reduced activity
98
- ([0, 0] + base_activity.take(3)).sample
99
- else # Weekday - normal patterns
100
- base_activity.sample
101
- end
102
-
103
- data[current_date.to_s] = score
104
-
105
- current_date += 1
106
- end
107
-
108
- data
109
- end
110
-
111
- # Run the generator
112
- if __FILE__ == $0
113
- generate_sample_svgs
114
- end
data/mise.toml DELETED
@@ -1,2 +0,0 @@
1
- [tools]
2
- ruby = "3.3"