heatmap-builder 0.2.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/CHANGELOG.md +70 -3
- data/Gemfile.lock +27 -24
- data/README.md +105 -135
- data/bin/generate_examples +42 -99
- data/examples/calendar_blue_ocean.svg +1 -1
- data/examples/calendar_cell_borders.svg +1 -0
- data/examples/calendar_default.svg +1 -1
- data/examples/calendar_github_style.svg +1 -1
- data/examples/calendar_month_spacing_rounded.svg +1 -0
- data/examples/calendar_no_borders.svg +1 -0
- data/examples/calendar_purple_vibes.svg +1 -1
- data/examples/calendar_red_to_green.svg +1 -1
- data/examples/calendar_rounded_corners.svg +1 -1
- data/examples/calendar_rounded_corners_max_radius.svg +1 -1
- data/examples/calendar_sunday_start.svg +1 -1
- data/examples/calendar_warm_sunset.svg +1 -1
- data/examples/calendar_with_outside_cells.svg +1 -1
- data/heatmap-builder.gemspec +4 -4
- data/lib/heatmap-builder.rb +5 -31
- data/lib/heatmap_builder/calendar.rb +426 -0
- data/lib/heatmap_builder/color_helpers.rb +121 -141
- data/lib/heatmap_builder/svg_helpers.rb +11 -4
- data/lib/heatmap_builder/value_conversion.rb +9 -5
- data/lib/heatmap_builder/version.rb +1 -1
- metadata +13 -22
- data/examples/large_cells.svg +0 -1
- data/examples/linear_blue_ocean.svg +0 -1
- data/examples/linear_github_green.svg +0 -1
- data/examples/linear_neon_gradient.svg +0 -1
- data/examples/linear_purple_vibes.svg +0 -1
- data/examples/linear_red_to_green.svg +0 -1
- data/examples/linear_rounded_corners.svg +0 -1
- data/examples/linear_rounded_corners_max_radius.svg +0 -1
- data/examples/linear_warm_sunset.svg +0 -1
- data/examples/weekly_progress.svg +0 -1
- data/lib/heatmap_builder/builder.rb +0 -100
- data/lib/heatmap_builder/calendar_heatmap_builder.rb +0 -310
- data/lib/heatmap_builder/linear_heatmap_builder.rb +0 -74
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
require_relative "svg_helpers"
|
|
2
|
-
require_relative "color_helpers"
|
|
3
|
-
|
|
4
|
-
module HeatmapBuilder
|
|
5
|
-
class Builder
|
|
6
|
-
include SvgHelpers
|
|
7
|
-
include ColorHelpers
|
|
8
|
-
|
|
9
|
-
GITHUB_GREEN = %w[#ebedf0 #9be9a8 #40c463 #30a14e #216e39].freeze
|
|
10
|
-
BLUE_OCEAN = %w[#f0f9ff #bae6fd #7dd3fc #38bdf8 #0ea5e9].freeze
|
|
11
|
-
WARM_SUNSET = %w[#fef3e2 #fed7aa #fdba74 #fb923c #f97316].freeze
|
|
12
|
-
PURPLE_VIBES = %w[#f3e8ff #d8b4fe #c084fc #a855f7 #9333ea].freeze
|
|
13
|
-
RED_TO_GREEN = %w[#f5f5f5 #ff9999 #f7ad6a #d2c768 #99dd99].freeze
|
|
14
|
-
|
|
15
|
-
DEFAULT_OPTIONS = {
|
|
16
|
-
cell_size: 10,
|
|
17
|
-
cell_spacing: 1,
|
|
18
|
-
font_size: 8,
|
|
19
|
-
border_width: 1,
|
|
20
|
-
corner_radius: 0,
|
|
21
|
-
colors: GITHUB_GREEN,
|
|
22
|
-
text_color: "#000000"
|
|
23
|
-
}.freeze
|
|
24
|
-
|
|
25
|
-
def initialize(scores: nil, values: nil, **options)
|
|
26
|
-
@scores = scores
|
|
27
|
-
@values = values
|
|
28
|
-
@options = default_options.merge(options)
|
|
29
|
-
validate_options!
|
|
30
|
-
normalize_options!
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def build
|
|
34
|
-
raise NotImplementedError, "Subclasses must implement #build"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
attr_reader :scores, :values, :options
|
|
40
|
-
|
|
41
|
-
def normalize_options!
|
|
42
|
-
max_radius = (options[:cell_size] / 2.0).floor
|
|
43
|
-
@options[:corner_radius] = options[:corner_radius].clamp(0, max_radius)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Override in subclasses to add specific validations by calling super first
|
|
47
|
-
def validate_options!
|
|
48
|
-
raise Error, "cell_size must be positive" unless options[:cell_size].positive?
|
|
49
|
-
raise Error, "font_size must be positive" unless options[:font_size].positive?
|
|
50
|
-
validate_colors_option!
|
|
51
|
-
validate_scores_or_values!
|
|
52
|
-
validate_value_boundaries! if values
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def validate_scores_or_values!
|
|
56
|
-
if scores && values
|
|
57
|
-
raise Error, "cannot provide both scores and values"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
unless scores || values
|
|
61
|
-
raise Error, "must provide either scores or values"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def validate_value_boundaries!
|
|
66
|
-
return unless options[:value_min] && options[:value_max]
|
|
67
|
-
return unless options[:value_min] > options[:value_max]
|
|
68
|
-
raise Error, "value_min must be less than or equal to value_max"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def validate_colors_option!
|
|
72
|
-
colors = options[:colors]
|
|
73
|
-
|
|
74
|
-
if colors.is_a?(Array)
|
|
75
|
-
raise Error, "must have at least 2 colors" unless colors.length >= 2
|
|
76
|
-
elsif colors.is_a?(Hash)
|
|
77
|
-
raise Error, "colors hash must have from, to, and steps keys" unless colors.key?(:from) && colors.key?(:to) && colors.key?(:steps)
|
|
78
|
-
raise Error, "steps must be a number" unless colors[:steps].is_a?(Integer)
|
|
79
|
-
raise Error, "steps must be at least 2" unless colors[:steps] >= 2
|
|
80
|
-
else
|
|
81
|
-
raise Error, "colors must be an array or hash with from/to/steps"
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def default_options
|
|
86
|
-
DEFAULT_OPTIONS
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def color_palette
|
|
90
|
-
@color_palette ||= begin
|
|
91
|
-
colors_option = options[:colors]
|
|
92
|
-
if colors_option.is_a?(Hash)
|
|
93
|
-
generate_color_palette(colors_option[:from], colors_option[:to], colors_option[:steps])
|
|
94
|
-
else
|
|
95
|
-
colors_option
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
require "date"
|
|
2
|
-
require_relative "builder"
|
|
3
|
-
require_relative "value_conversion"
|
|
4
|
-
|
|
5
|
-
module HeatmapBuilder
|
|
6
|
-
class CalendarHeatmapBuilder < Builder
|
|
7
|
-
include ValueConversion
|
|
8
|
-
|
|
9
|
-
VALID_START_DAYS = %i[sunday monday tuesday wednesday thursday friday saturday].freeze
|
|
10
|
-
|
|
11
|
-
WEEK_START_WDAY = {
|
|
12
|
-
sunday: 0,
|
|
13
|
-
monday: 1,
|
|
14
|
-
tuesday: 2,
|
|
15
|
-
wednesday: 3,
|
|
16
|
-
thursday: 4,
|
|
17
|
-
friday: 5,
|
|
18
|
-
saturday: 6
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
def build
|
|
22
|
-
svg_content = []
|
|
23
|
-
|
|
24
|
-
if options[:show_day_labels]
|
|
25
|
-
svg_content << day_labels_svg
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
svg_content << calendar_cells_svg
|
|
29
|
-
|
|
30
|
-
if options[:show_month_labels]
|
|
31
|
-
svg_content << month_labels_svg
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
weeks_count = ((calendar_end_date_with_full_weeks - calendar_start_date) / 7).ceil
|
|
35
|
-
month_spacing_total = (months_in_range - 1) * options[:month_spacing]
|
|
36
|
-
|
|
37
|
-
cell_size_with_spacing = options[:cell_size] + options[:cell_spacing]
|
|
38
|
-
width = dow_label_offset + weeks_count * cell_size_with_spacing + month_spacing_total
|
|
39
|
-
height = month_label_offset + 7 * cell_size_with_spacing
|
|
40
|
-
|
|
41
|
-
svg_container(width: width, height: height) { svg_content.join }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def start_date
|
|
47
|
-
@start_date ||= parsed_date_range.first
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def end_date
|
|
51
|
-
@end_date ||= parsed_date_range.last
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def validate_options!
|
|
55
|
-
super
|
|
56
|
-
|
|
57
|
-
raise Error, "scores must be a hash" if scores && !scores.is_a?(Hash)
|
|
58
|
-
raise Error, "values must be a hash" if values && !values.is_a?(Hash)
|
|
59
|
-
|
|
60
|
-
unless VALID_START_DAYS.include?(options[:start_of_week])
|
|
61
|
-
raise Error, "start_of_week must be one of: #{VALID_START_DAYS.join(", ")}"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def scores_by_date
|
|
66
|
-
@scores_by_date ||= if scores
|
|
67
|
-
scores
|
|
68
|
-
else
|
|
69
|
-
result = {}
|
|
70
|
-
current_date = start_date
|
|
71
|
-
|
|
72
|
-
while current_date <= end_date
|
|
73
|
-
value = values[current_date] || values[current_date.to_s]
|
|
74
|
-
result[current_date] = convert_value_to_score(value, date: current_date)
|
|
75
|
-
current_date += 1
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
result
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def calculated_min_from_values
|
|
83
|
-
non_nil_values = values.values.compact
|
|
84
|
-
non_nil_values.empty? ? 0 : non_nil_values.min
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def calculated_max_from_values
|
|
88
|
-
non_nil_values = values.values.compact
|
|
89
|
-
non_nil_values.empty? ? 0 : non_nil_values.max
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def parsed_date_range
|
|
93
|
-
@parsed_date_range ||= begin
|
|
94
|
-
data_keys = (scores || values || {}).keys
|
|
95
|
-
dates = data_keys.map { |d| d.is_a?(Date) ? d : Date.parse(d.to_s) }
|
|
96
|
-
return [Date.today - 365, Date.today] if dates.empty?
|
|
97
|
-
|
|
98
|
-
[dates.min, dates.max]
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def calendar_cells_svg
|
|
103
|
-
svg = ""
|
|
104
|
-
current_date = calendar_start_date
|
|
105
|
-
week_index = 0
|
|
106
|
-
last_month = nil
|
|
107
|
-
current_x_offset = 0
|
|
108
|
-
calendar_end_date = calendar_end_date_with_full_weeks
|
|
109
|
-
|
|
110
|
-
while current_date <= calendar_end_date
|
|
111
|
-
# Check if we need to add month spacing
|
|
112
|
-
if current_date.month != last_month && !last_month.nil?
|
|
113
|
-
current_x_offset += options[:month_spacing]
|
|
114
|
-
end
|
|
115
|
-
last_month = current_date.month
|
|
116
|
-
|
|
117
|
-
# Generate week column - always fill all 7 days
|
|
118
|
-
7.times do |day_index|
|
|
119
|
-
x = dow_label_offset + week_index * (options[:cell_size] + options[:cell_spacing]) + current_x_offset
|
|
120
|
-
y = month_label_offset + day_index * (options[:cell_size] + options[:cell_spacing])
|
|
121
|
-
|
|
122
|
-
if current_date.between?(start_date, end_date)
|
|
123
|
-
# Active cell within the specified timeframe
|
|
124
|
-
score = scores_by_date[current_date] || scores_by_date[current_date.to_s] || 0
|
|
125
|
-
svg << cell_svg(score, x, y, false)
|
|
126
|
-
elsif options[:show_outside_cells]
|
|
127
|
-
# Inactive cell outside the specified timeframe
|
|
128
|
-
svg << cell_svg(0, x, y, true)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
current_date += 1
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
week_index += 1
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
svg
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def cell_svg(score, x, y, inactive = false)
|
|
141
|
-
color = score_to_color(score, colors: color_palette)
|
|
142
|
-
|
|
143
|
-
if inactive
|
|
144
|
-
color = make_color_inactive(color)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
colored_rect = svg_rect(
|
|
148
|
-
x: x, y: y,
|
|
149
|
-
width: options[:cell_size], height: options[:cell_size],
|
|
150
|
-
rx: options[:corner_radius],
|
|
151
|
-
fill: color
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
border_rect = cell_border(
|
|
155
|
-
x, y, color,
|
|
156
|
-
cell_size: options[:cell_size],
|
|
157
|
-
border_width: options[:border_width],
|
|
158
|
-
corner_radius: options[:corner_radius]
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
"#{colored_rect}#{border_rect}"
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def day_labels_svg
|
|
165
|
-
return "" unless options[:show_day_labels]
|
|
166
|
-
|
|
167
|
-
day_names = day_names_for_week_start
|
|
168
|
-
svg = ""
|
|
169
|
-
|
|
170
|
-
day_names.each_with_index do |day_name, index|
|
|
171
|
-
y = month_label_offset + index * (options[:cell_size] + options[:cell_spacing]) + options[:cell_size] / 2 + options[:font_size] * 0.35
|
|
172
|
-
svg << svg_text(
|
|
173
|
-
day_name,
|
|
174
|
-
x: options[:font_size], y: y,
|
|
175
|
-
font_size: options[:font_size], fill: "#666666"
|
|
176
|
-
)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
svg
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def month_labels_svg
|
|
183
|
-
return "" unless options[:show_month_labels]
|
|
184
|
-
|
|
185
|
-
svg = ""
|
|
186
|
-
last_month = nil
|
|
187
|
-
|
|
188
|
-
each_week do |current_date, _week_index|
|
|
189
|
-
current_month = [current_date.year, current_date.month]
|
|
190
|
-
|
|
191
|
-
if current_month != last_month && month_overlaps_timeframe?(current_date)
|
|
192
|
-
svg << render_month_label(current_date)
|
|
193
|
-
last_month = current_month
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
svg
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def month_overlaps_timeframe?(current_date)
|
|
201
|
-
month_start = Date.new(current_date.year, current_date.month, 1)
|
|
202
|
-
month_end = Date.new(current_date.year, current_date.month, -1)
|
|
203
|
-
|
|
204
|
-
month_start <= end_date && month_end >= start_date
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def render_month_label(current_date)
|
|
208
|
-
first_day_of_month = Date.new(current_date.year, current_date.month, 1)
|
|
209
|
-
month_name = options[:month_labels][current_date.month - 1]
|
|
210
|
-
|
|
211
|
-
svg_text(
|
|
212
|
-
month_name,
|
|
213
|
-
x: calculate_month_label_x(first_day_of_month),
|
|
214
|
-
y: calculate_month_label_y,
|
|
215
|
-
text_anchor: "start", font_family: "Arial, sans-serif", font_size: options[:font_size], fill: "#666666"
|
|
216
|
-
)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def calculate_month_label_y
|
|
220
|
-
options[:font_size] * 1.25
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def calculate_month_label_x(first_day_of_month)
|
|
224
|
-
days_from_start = (first_day_of_month - calendar_start_date).to_i
|
|
225
|
-
week_index = days_from_start / 7
|
|
226
|
-
x_offset = calculate_x_offset_for_week(week_index)
|
|
227
|
-
|
|
228
|
-
dow_label_offset + week_index * (options[:cell_size] + options[:cell_spacing]) + x_offset + options[:cell_size] * 0.1
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def calculate_x_offset_for_week(target_week_index)
|
|
232
|
-
x_offset = 0
|
|
233
|
-
last_month = nil
|
|
234
|
-
|
|
235
|
-
each_week do |current_date, week_index|
|
|
236
|
-
break if week_index >= target_week_index
|
|
237
|
-
|
|
238
|
-
if current_date.month != last_month && !last_month.nil?
|
|
239
|
-
x_offset += options[:month_spacing]
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
last_month = current_date.month
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
x_offset
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def each_week
|
|
249
|
-
current_date = calendar_start_date
|
|
250
|
-
week_index = 0
|
|
251
|
-
|
|
252
|
-
while current_date <= calendar_end_date_with_full_weeks
|
|
253
|
-
yield current_date, week_index
|
|
254
|
-
|
|
255
|
-
current_date += 7
|
|
256
|
-
week_index += 1
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# Find the start of the week containing start_date
|
|
261
|
-
def calendar_start_date
|
|
262
|
-
days_back = (start_date.wday - week_start_wday) % 7
|
|
263
|
-
start_date - days_back
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Find the end of the week containing end_date
|
|
267
|
-
def calendar_end_date_with_full_weeks
|
|
268
|
-
days_forward = (6 - (end_date.wday - week_start_wday)) % 7
|
|
269
|
-
end_date + days_forward
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def week_start_wday
|
|
273
|
-
WEEK_START_WDAY[options[:start_of_week]]
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def day_names_for_week_start
|
|
277
|
-
start_index = week_start_wday
|
|
278
|
-
options[:day_labels].rotate(start_index)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def month_spacing_weeks
|
|
282
|
-
options[:month_spacing] / (options[:cell_size] + options[:cell_spacing])
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def dow_label_offset
|
|
286
|
-
options[:show_day_labels] ? options[:font_size] * 2 : 0
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def month_label_offset
|
|
290
|
-
options[:show_month_labels] ? options[:font_size] * 1.625 : 0
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def months_in_range
|
|
294
|
-
((end_date.year - start_date.year) * 12 + end_date.month - start_date.month + 1)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def default_options
|
|
298
|
-
DEFAULT_OPTIONS.merge({
|
|
299
|
-
cell_size: 12,
|
|
300
|
-
start_of_week: :monday,
|
|
301
|
-
month_spacing: 5, # extra horizontal space between months
|
|
302
|
-
show_month_labels: true,
|
|
303
|
-
show_day_labels: true,
|
|
304
|
-
show_outside_cells: false, # show cells outside the timeframe with inactive styling
|
|
305
|
-
day_labels: %w[S M T W T F S], # day abbreviations starting from Sunday
|
|
306
|
-
month_labels: %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec] # month abbreviations
|
|
307
|
-
})
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
require_relative "builder"
|
|
2
|
-
require_relative "value_conversion"
|
|
3
|
-
|
|
4
|
-
module HeatmapBuilder
|
|
5
|
-
class LinearHeatmapBuilder < Builder
|
|
6
|
-
include ValueConversion
|
|
7
|
-
|
|
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 }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def validate_options!
|
|
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)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def computed_scores
|
|
29
|
-
@computed_scores ||= scores || values.map.with_index { |value, index| convert_value_to_score(value, index: index) }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def calculated_min_from_values
|
|
33
|
-
non_nil_values = values.compact
|
|
34
|
-
non_nil_values.empty? ? 0 : non_nil_values.min
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def calculated_max_from_values
|
|
38
|
-
non_nil_values = values.compact
|
|
39
|
-
non_nil_values.empty? ? 0 : non_nil_values.max
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def cell_svg(score, index)
|
|
43
|
-
x = index * (options[:cell_size] + options[:cell_spacing])
|
|
44
|
-
y = 0
|
|
45
|
-
|
|
46
|
-
color = score_to_color(score, colors: color_palette)
|
|
47
|
-
|
|
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
|
-
)
|
|
54
|
-
|
|
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
|
-
)
|
|
61
|
-
|
|
62
|
-
text_x = x + options[:cell_size] / 2
|
|
63
|
-
text_y = y + options[:cell_size] / 2 + options[:font_size] * 0.35
|
|
64
|
-
|
|
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
|
-
)
|
|
70
|
-
|
|
71
|
-
"#{colored_rect}#{border_rect}#{text_element}"
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|