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.
@@ -0,0 +1,68 @@
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
+ if options[:value_to_score]
36
+ score = options[:value_to_score].call(
37
+ value: value.nil? ? value_min : value,
38
+ min: value_min,
39
+ max: value_max,
40
+ max_score: color_count - 1,
41
+ **params
42
+ )
43
+
44
+ unless score.is_a?(Integer) && score >= 0 && score < color_count
45
+ raise Error, "value_to_score must return an integer between 0 and #{color_count - 1}, got #{score.inspect}"
46
+ end
47
+
48
+ return score
49
+ end
50
+
51
+ # Score 0 is reserved for empty cells (zero or missing values). Any
52
+ # non-zero value maps into 1..max_score so even the smallest activity is
53
+ # visibly distinct from an empty day.
54
+ return 0 if value.nil? || value.zero?
55
+
56
+ max_score = color_count - 1
57
+ clamped_value = value.clamp(value_min, value_max)
58
+
59
+ if value_min == value_max
60
+ max_score
61
+ else
62
+ range = value_max - value_min
63
+ normalized = (clamped_value - value_min).to_f / range
64
+ 1 + (normalized * (max_score - 1)).round
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module HeatmapBuilder
2
- VERSION = "0.1.0"
2
+ VERSION = "0.4.3"
3
3
  end
metadata CHANGED
@@ -1,55 +1,55 @@
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.4.3
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: 2026-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '5.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
@@ -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,28 @@ 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_cell_borders.svg
106
+ - examples/calendar_default.svg
89
107
  - examples/calendar_github_style.svg
108
+ - examples/calendar_month_spacing_rounded.svg
109
+ - examples/calendar_no_borders.svg
110
+ - examples/calendar_purple_vibes.svg
111
+ - examples/calendar_red_to_green.svg
112
+ - examples/calendar_rounded_corners.svg
113
+ - examples/calendar_rounded_corners_max_radius.svg
90
114
  - examples/calendar_sunday_start.svg
115
+ - examples/calendar_warm_sunset.svg
91
116
  - examples/calendar_with_outside_cells.svg
92
- - examples/generate_samples.rb
93
- - examples/large_cells.svg
94
- - examples/weekly_progress.svg
95
117
  - heatmap-builder.gemspec
96
118
  - lib/heatmap-builder.rb
97
- - lib/heatmap_builder/calendar_heatmap_builder.rb
98
- - lib/heatmap_builder/linear_heatmap_builder.rb
119
+ - lib/heatmap_builder/calendar.rb
120
+ - lib/heatmap_builder/color_helpers.rb
121
+ - lib/heatmap_builder/svg_helpers.rb
122
+ - lib/heatmap_builder/value_conversion.rb
99
123
  - lib/heatmap_builder/version.rb
100
- - mise.toml
101
124
  homepage: https://github.com/dreikanter/heatmap-builder
102
125
  licenses:
103
126
  - MIT
@@ -114,7 +137,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
137
  requirements:
115
138
  - - ">="
116
139
  - !ruby/object:Gem::Version
117
- version: '3.0'
140
+ version: '3.3'
118
141
  required_rubygems_version: !ruby/object:Gem::Requirement
119
142
  requirements:
120
143
  - - ">="
@@ -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
@@ -1,3 +0,0 @@
1
- <svg width="251" height="35" xmlns="http://www.w3.org/2000/svg">
2
- <rect x="0" y="0" width="35" height="35" fill="#9be9a8"/><rect x="0.5" y="0.5" width="34" height="34" fill="none" stroke="#6ca375" stroke-width="1"/><text x="17" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#000000">1</text><rect x="36" y="0" width="35" height="35" fill="#40c463"/><rect x="36.5" y="0.5" width="34" height="34" fill="none" stroke="#2c8945" stroke-width="1"/><text x="53" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#000000">2</text><rect x="72" y="0" width="35" height="35" fill="#30a14e"/><rect x="72.5" y="0.5" width="34" height="34" fill="none" stroke="#217036" stroke-width="1"/><text x="89" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#ffffff">3</text><rect x="108" y="0" width="35" height="35" fill="#216e39"/><rect x="108.5" y="0.5" width="34" height="34" fill="none" stroke="#174d27" stroke-width="1"/><text x="125" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#ffffff">4</text><rect x="144" y="0" width="35" height="35" fill="#9be9a8"/><rect x="144.5" y="0.5" width="34" height="34" fill="none" stroke="#6ca375" stroke-width="1"/><text x="161" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#000000">5</text><rect x="180" y="0" width="35" height="35" fill="#40c463"/><rect x="180.5" y="0.5" width="34" height="34" fill="none" stroke="#2c8945" stroke-width="1"/><text x="197" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#000000">6</text><rect x="216" y="0" width="35" height="35" fill="#30a14e"/><rect x="216.5" y="0.5" width="34" height="34" fill="none" stroke="#217036" stroke-width="1"/><text x="233" y="24.0" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="#ffffff">7</text>
3
- </svg>
@@ -1,3 +0,0 @@
1
- <svg width="132" height="18" xmlns="http://www.w3.org/2000/svg">
2
- <rect x="0" y="0" width="18" height="18" fill="#ebedf0"/><rect x="0.5" y="0.5" width="17" height="17" fill="none" stroke="#a4a5a8" stroke-width="1"/><text x="9" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#000000">0</text><rect x="19" y="0" width="18" height="18" fill="#9be9a8"/><rect x="19.5" y="0.5" width="17" height="17" fill="none" stroke="#6ca375" stroke-width="1"/><text x="28" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#000000">1</text><rect x="38" y="0" width="18" height="18" fill="#30a14e"/><rect x="38.5" y="0.5" width="17" height="17" fill="none" stroke="#217036" stroke-width="1"/><text x="47" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#ffffff">3</text><rect x="57" y="0" width="18" height="18" fill="#40c463"/><rect x="57.5" y="0.5" width="17" height="17" fill="none" stroke="#2c8945" stroke-width="1"/><text x="66" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#000000">2</text><rect x="76" y="0" width="18" height="18" fill="#216e39"/><rect x="76.5" y="0.5" width="17" height="17" fill="none" stroke="#174d27" stroke-width="1"/><text x="85" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#ffffff">4</text><rect x="95" y="0" width="18" height="18" fill="#9be9a8"/><rect x="95.5" y="0.5" width="17" height="17" fill="none" stroke="#6ca375" stroke-width="1"/><text x="104" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#000000">1</text><rect x="114" y="0" width="18" height="18" fill="#ebedf0"/><rect x="114.5" y="0.5" width="17" height="17" fill="none" stroke="#a4a5a8" stroke-width="1"/><text x="123" y="11.8" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#000000">0</text>
3
- </svg>
@@ -1,295 +0,0 @@
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