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
|
@@ -1,128 +0,0 @@
|
|
|
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
DELETED