dates2svg 0.0.1.beta1 → 0.0.1.beta2
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +5 -0
- data/README.md +30 -2
- data/Rakefile +4 -0
- data/lib/dates2svg.rb +80 -69
- data/lib/dates2svg/version.rb +1 -1
- data/spec/lib/dates2svg_spec.rb +44 -13
- metadata +3 -2
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/jkeck/dates2svg.png?branch=master)](https://travis-ci.org/jkeck/dates2svg)
|
2
|
+
|
1
3
|
# Dates2svg
|
2
4
|
|
3
|
-
|
5
|
+
Turn an array of simple objects including dates and values into an SVG month grid with heat-map.
|
6
|
+
|
7
|
+
![date range heat map](http://i.imgur.com/6dcL09C.png)
|
4
8
|
|
5
9
|
## Installation
|
6
10
|
|
@@ -18,7 +22,31 @@ Or install it yourself as:
|
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
21
|
-
|
25
|
+
You will need an array of objects that respond to value with a string in a YYYY-MM-DD format (things at the end like timestamps are okay) and respond to hits with a number.
|
26
|
+
|
27
|
+
dates = [OpenStruct.new(:value => "2013-01-22", :hits => 100),
|
28
|
+
OpenStruct.new(:value => "2013-01-05", :hits => 140),
|
29
|
+
OpenStruct.new(:value => "2013-01-12", :hits => 10)]
|
30
|
+
|
31
|
+
Passing that array to Dates2SVG.new will give you your object that you can get the SVG from.
|
32
|
+
|
33
|
+
svg = Dates2SVG.new(dates).to_svg
|
34
|
+
|
35
|
+
### Options
|
36
|
+
|
37
|
+
There are various options that can be passed in as an options has upon initialization. These options are year_range, color_options, box_size, and border.
|
38
|
+
|
39
|
+
By default you will only get years that occur in the data passed in the dates array. You can specify a range of years by passing a year_range option.
|
40
|
+
|
41
|
+
svg = Dates2SVG.new(dates, :year_range => (2000..2020)).to_svg
|
42
|
+
|
43
|
+
If you would like to change the colors that the grid uses for the heat-map you can specify different colors in the color_options option.
|
44
|
+
|
45
|
+
svg = Dates2SVG.new(dates, :color_options => ["black", "purple", "blue", "green", "yellow", "red"]).to_svg
|
46
|
+
|
47
|
+
You can see the current range of colors and hits ranges being used in the SVG by accessing in the color_range key in the options hash. This would be useful in generating a legend.
|
48
|
+
|
49
|
+
color_range = Dates2SVG.new(dates).options[:color_range]
|
22
50
|
|
23
51
|
## Contributing
|
24
52
|
|
data/Rakefile
CHANGED
data/lib/dates2svg.rb
CHANGED
@@ -1,28 +1,14 @@
|
|
1
1
|
require "dates2svg/version"
|
2
2
|
|
3
3
|
class Dates2SVG
|
4
|
-
|
5
|
-
BORDER = 1
|
6
|
-
BOX_WITH_BORDER = (BOX_SIZE + BORDER)
|
4
|
+
attr_reader :options
|
7
5
|
def initialize(dates, options={})
|
8
6
|
@dates = dates
|
9
7
|
@options = options
|
8
|
+
process_options
|
10
9
|
parse_dates
|
11
|
-
|
12
|
-
|
13
|
-
def parse_dates
|
14
|
-
@dates.map do |date|
|
15
|
-
date_from_facet_value(date)
|
16
|
-
end.sort do |a, b|
|
17
|
-
a.year.to_i <=> b.year.to_i
|
18
|
-
end.group_by(&:year).each do |year, dates|
|
19
|
-
years << Dates2SVG::Year.new(year, dates)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def date_from_facet_value(facet)
|
24
|
-
facet.value[/^(\d{4})-(\d{2})-(\d{2})/]
|
25
|
-
Dates2SVG::DateWithValue.new(:year => $1, :month => $2, :day => $3, :hits => facet.hits)
|
10
|
+
@options[:color_range] = color_range
|
11
|
+
@options[:max] = max
|
26
12
|
end
|
27
13
|
|
28
14
|
def years
|
@@ -34,24 +20,41 @@ class Dates2SVG
|
|
34
20
|
end
|
35
21
|
|
36
22
|
def color_range
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
23
|
+
max_hits = max || 3000
|
24
|
+
section = (max_hits) / (@options[:color_options].length - 1)
|
25
|
+
colors = {}
|
26
|
+
ranges = [[0]]
|
27
|
+
|
28
|
+
i = 0
|
29
|
+
(@options[:color_options].length - 1).times do
|
30
|
+
if i == 0
|
31
|
+
ranges << (1..(section * 1))
|
32
|
+
elsif ((i+1) == (@options[:color_options].length - 1))
|
33
|
+
ranges << (((section * i) + 1)..max_hits)
|
34
|
+
else
|
35
|
+
ranges << (((section * i) + 1)..(section * (i + 1)))
|
36
|
+
end
|
37
|
+
i = i+1
|
38
|
+
end
|
39
|
+
@options[:color_options].each_with_index do |color, index|
|
40
|
+
unless ranges[index].is_a?(Range) and [ranges[index].first, ranges[index].last].include?(0)
|
41
|
+
colors[ranges[index]] = color
|
42
|
+
end
|
43
|
+
end
|
44
|
+
colors
|
42
45
|
end
|
43
|
-
|
46
|
+
|
44
47
|
def to_svg
|
45
48
|
svg = ""
|
46
49
|
# + 35 and +50 gives us the space for the year month text
|
47
|
-
svg << "<svg height='#{(12 *
|
50
|
+
svg << "<svg height='#{(12 * @options[:box_with_border]) + 35}' width='#{(all_years.to_a.length * @options[:box_with_border]) + 50}'>"
|
48
51
|
svg << "<g transform='translate(20,35)'>"
|
49
52
|
all_years.each_with_index do |year, index|
|
50
53
|
selected_years = years.select{|y| y.year.to_i == year.to_i }
|
51
54
|
unless selected_years.length == 0
|
52
|
-
svg << selected_years.first.to_svg(
|
55
|
+
svg << selected_years.first.to_svg(:index => index)
|
53
56
|
else
|
54
|
-
svg << Dates2SVG::Year.new(year, []).to_svg(
|
57
|
+
svg << Dates2SVG::Year.new(year, [], @options).to_svg(:index => index)
|
55
58
|
end
|
56
59
|
end
|
57
60
|
svg << calendar_year_listing
|
@@ -60,30 +63,23 @@ class Dates2SVG
|
|
60
63
|
svg << "</svg>"
|
61
64
|
end
|
62
65
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
(((section * 4) + 1)..max_hits)]
|
73
|
-
color_options.each_with_index do |color, index|
|
74
|
-
unless ranges[index].is_a?(Range) and [ranges[index].first, ranges[index].last].include?(0)
|
75
|
-
colors[ranges[index]] = color
|
76
|
-
end
|
66
|
+
private
|
67
|
+
|
68
|
+
def parse_dates
|
69
|
+
@dates.map do |date|
|
70
|
+
date_from_facet_value(date)
|
71
|
+
end.sort do |a, b|
|
72
|
+
a.year.to_i <=> b.year.to_i
|
73
|
+
end.group_by(&:year).each do |year, dates|
|
74
|
+
years << Dates2SVG::Year.new(year, dates, @options)
|
77
75
|
end
|
78
|
-
colors
|
79
76
|
end
|
80
|
-
|
81
|
-
def
|
82
|
-
|
77
|
+
|
78
|
+
def date_from_facet_value(facet)
|
79
|
+
facet.value[/^(\d{4})-(\d{2})-(\d{2})/]
|
80
|
+
Dates2SVG::DateWithValue.new(:year => $1, :month => $2, :day => $3, :hits => facet.hits)
|
83
81
|
end
|
84
|
-
|
85
|
-
private
|
86
|
-
|
82
|
+
|
87
83
|
def all_years
|
88
84
|
@options[:year_range] || years.map{|y| y.year }
|
89
85
|
end
|
@@ -91,7 +87,7 @@ class Dates2SVG
|
|
91
87
|
def calendar_year_listing
|
92
88
|
svg = ""
|
93
89
|
all_years.each_with_index do |year, index|
|
94
|
-
svg << "<text x='3' y='#{(
|
90
|
+
svg << "<text x='3' y='#{(@options[:box_with_border] * index) + 20}' transform='rotate(-90)' class='year'>#{year}</text>"
|
95
91
|
end
|
96
92
|
svg
|
97
93
|
end
|
@@ -99,11 +95,24 @@ class Dates2SVG
|
|
99
95
|
def calendar_month_listing
|
100
96
|
svg = ""
|
101
97
|
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", " Oct", "Nov", "Dec"].each_with_index do |month, index|
|
102
|
-
svg << "<text text-anchor='middle' class='cmonth' dx='0' dy='#{(
|
98
|
+
svg << "<text text-anchor='middle' class='cmonth' dx='0' dy='#{(@options[:box_with_border] * index) + 10}'>#{month}</text>"
|
103
99
|
end
|
104
100
|
svg
|
105
101
|
end
|
106
102
|
|
103
|
+
def process_options
|
104
|
+
@options = self.class.default_options.merge(@options)
|
105
|
+
@options[:box_with_border] = (@options[:box_size].to_i + @options[:border].to_i)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.default_options
|
109
|
+
{:color_options => ["#EEEEEE", "#330000", "#660000", "#990000", "#CC0000", "#FF0000"],
|
110
|
+
:box_size => 15,
|
111
|
+
:border => 1,
|
112
|
+
:box_with_border => nil
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
107
116
|
class DateWithValue
|
108
117
|
attr_reader :year, :month, :day, :hits
|
109
118
|
def initialize(ops={})
|
@@ -116,32 +125,25 @@ class Dates2SVG
|
|
116
125
|
|
117
126
|
class Year
|
118
127
|
attr_reader :year
|
119
|
-
def initialize(year, dates)
|
128
|
+
def initialize(year, dates, options={})
|
120
129
|
@year = year
|
121
130
|
@dates = dates
|
131
|
+
@options = options
|
122
132
|
parse_dates
|
123
133
|
end
|
124
134
|
|
125
|
-
def parse_dates
|
126
|
-
@dates.sort do |a, b|
|
127
|
-
a.month.to_i <=> b.month.to_i
|
128
|
-
end.group_by(&:month).map do |month, dates|
|
129
|
-
months << Dates2SVG::Month.new(@year, month, dates)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
135
|
def months
|
134
136
|
@months ||= []
|
135
137
|
end
|
136
138
|
|
137
139
|
def to_svg(options={})
|
138
140
|
svg = ""
|
139
|
-
svg << "<g transform='translate(#{(
|
141
|
+
svg << "<g transform='translate(#{(@options[:box_with_border] * options[:index]) + 12},0)'>"
|
140
142
|
all_months.each_with_index do |month, index|
|
141
143
|
unless months.select{|m| m.month == month}.length == 0
|
142
|
-
svg << months.select{|m| m.month == month}.first.to_svg(
|
144
|
+
svg << months.select{|m| m.month == month}.first.to_svg(:index => index)
|
143
145
|
else
|
144
|
-
svg << Dates2SVG::Month.new(@year, month, []).to_svg(
|
146
|
+
svg << Dates2SVG::Month.new(@year, month, [], @options).to_svg(:index => index)
|
145
147
|
end
|
146
148
|
end
|
147
149
|
svg << "</g>"
|
@@ -149,6 +151,14 @@ class Dates2SVG
|
|
149
151
|
|
150
152
|
private
|
151
153
|
|
154
|
+
def parse_dates
|
155
|
+
@dates.sort do |a, b|
|
156
|
+
a.month.to_i <=> b.month.to_i
|
157
|
+
end.group_by(&:month).map do |month, dates|
|
158
|
+
months << Dates2SVG::Month.new(@year, month, dates, @options)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
152
162
|
def all_months
|
153
163
|
["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]
|
154
164
|
end
|
@@ -157,10 +167,11 @@ class Dates2SVG
|
|
157
167
|
|
158
168
|
class Month
|
159
169
|
attr_reader :year, :month
|
160
|
-
def initialize(year, month, dates)
|
170
|
+
def initialize(year, month, dates, options={})
|
161
171
|
@year = year
|
162
172
|
@month = month
|
163
173
|
@dates = dates
|
174
|
+
@options = options
|
164
175
|
end
|
165
176
|
def hits
|
166
177
|
@hits ||= (@dates.map{|d| d.hits.to_i }.inject(:+) || 0)
|
@@ -168,25 +179,25 @@ class Dates2SVG
|
|
168
179
|
|
169
180
|
def to_svg(options={})
|
170
181
|
svg = ""
|
171
|
-
svg << "<rect data-hits='#{hits}' data-year='#{@year}' data-month='#{@month}' class='month' width='#{
|
182
|
+
svg << "<rect data-hits='#{hits}' data-year='#{@year}' data-month='#{@month}' class='month' width='#{@options[:box_size]}' height='#{@options[:box_size]}' y='#{@options[:box_with_border] * options[:index]}' style='fill: #{color};'>"
|
172
183
|
svg << "<title>#{hits} hits on #{@year}-#{@month}</title>"
|
173
184
|
svg << "</rect>"
|
174
185
|
end
|
175
186
|
|
176
187
|
private
|
177
188
|
|
178
|
-
def color
|
179
|
-
max_hits = options[:max] || 3000
|
189
|
+
def color
|
190
|
+
max_hits = @options[:max] || 3000
|
180
191
|
section = (max_hits / 5)
|
181
192
|
max = section * 5
|
182
193
|
|
183
|
-
return
|
184
|
-
colors =
|
194
|
+
return @options[:color_options].last if hits > max
|
195
|
+
colors = @options[:color_range]
|
185
196
|
begin
|
186
197
|
# .first turns the hash into an array and .last gets the value
|
187
198
|
colors.select{|k,v| k.include?(hits) }.first.last
|
188
199
|
rescue
|
189
|
-
|
200
|
+
@options[:color_options].first
|
190
201
|
end
|
191
202
|
end
|
192
203
|
end
|
data/lib/dates2svg/version.rb
CHANGED
data/spec/lib/dates2svg_spec.rb
CHANGED
@@ -76,33 +76,64 @@ describe Dates2SVG do
|
|
76
76
|
|
77
77
|
describe "colors" do
|
78
78
|
describe "color_range" do
|
79
|
-
it "should
|
80
|
-
range = Dates2SVG.color_range
|
81
|
-
range.keys.first.should == [0]
|
82
|
-
range[[0]].should == "#EEEEEE"
|
79
|
+
it "should use the default maximum of 3000 if one isn't available (althouth that shouldn't happen)" do
|
80
|
+
range = Dates2SVG.new([]).options[:color_range]
|
83
81
|
range.keys.last.should == (2401..3000)
|
84
82
|
range[(2401..3000)].should == "#FF0000"
|
85
83
|
end
|
86
|
-
it "should
|
87
|
-
range =
|
84
|
+
it "should be from 0 - the max number of hits" do
|
85
|
+
range = @ranges.options[:color_range]
|
88
86
|
range.keys.first.should == [0]
|
89
87
|
range[[0]].should == "#EEEEEE"
|
90
|
-
range.keys.last.should == (
|
91
|
-
range[(
|
88
|
+
range.keys.last.should == (1197..1499)
|
89
|
+
range[(1197..1499)].should == "#FF0000"
|
92
90
|
end
|
93
91
|
it "should use an updated color range if one is provided" do
|
94
92
|
colors = ["black", "purple", "blue", "green", "yellow", "red"]
|
95
|
-
range = Dates2SVG.
|
93
|
+
range = Dates2SVG.new([], :color_options => colors).options[:color_range]
|
96
94
|
range.keys.first.should == [0]
|
97
95
|
range[[0]].should == colors.first
|
98
|
-
range.keys.last.should == (
|
99
|
-
range[(
|
96
|
+
range.keys.last.should == (2401..3000)
|
97
|
+
range[(2401..3000)].should == colors.last
|
98
|
+
end
|
99
|
+
it "should have more range options if additional colors are passed" do
|
100
|
+
colors = ["black", "purple", "blue", "green", "yellow", "red"]
|
101
|
+
range = Dates2SVG.new([], :color_options => colors).options[:color_range]
|
102
|
+
range.keys.length.should == colors.length
|
103
|
+
colors << ["#FF0000", "grey"]
|
104
|
+
range = Dates2SVG.new([], :color_options => colors).options[:color_range]
|
105
|
+
range.keys.length.should == colors.length
|
100
106
|
end
|
101
107
|
end
|
102
108
|
end
|
103
109
|
|
104
110
|
describe "svg export" do
|
105
111
|
describe "options" do
|
112
|
+
describe "box size" do
|
113
|
+
it "should default to a 15px box size" do
|
114
|
+
@ranges.to_svg.should match(/<rect.*width='15' height='15'.*>/)
|
115
|
+
end
|
116
|
+
it "should change the box size when a box_size option is available" do
|
117
|
+
new_range = Dates2SVG.new(@dates, :box_size => "10").to_svg
|
118
|
+
new_range.should_not match(/<rect.*width='15' height='15'.*>/)
|
119
|
+
new_range.should match(/<rect.*width='10' height='10'.*>/)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
describe "border" do
|
123
|
+
it "should default to a 1px border" do
|
124
|
+
@ranges.to_svg.should include "<g transform='translate(28,0)'>"
|
125
|
+
end
|
126
|
+
it "should change the border when a border options is available" do
|
127
|
+
border_2 = Dates2SVG.new(@dates, :border => "2").to_svg
|
128
|
+
border_2.should_not include "<g transform='translate(28,0)'>"
|
129
|
+
border_2.should include "<g transform='translate(29,0)'>"
|
130
|
+
|
131
|
+
border_4 = Dates2SVG.new(@dates, :border => "4").to_svg
|
132
|
+
border_4.should_not include "<g transform='translate(28,0)'>"
|
133
|
+
border_4.should include "<g transform='translate(31,0)'>"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
106
137
|
describe "year_range" do
|
107
138
|
it "should return only the requested years" do
|
108
139
|
@ranges.to_svg.should match(/data-year='1779'/)
|
@@ -112,12 +143,12 @@ describe Dates2SVG do
|
|
112
143
|
end
|
113
144
|
describe "color_options" do
|
114
145
|
it "use the range of color provided" do
|
115
|
-
[
|
146
|
+
[@ranges.options[:color_options].first, @ranges.options[:color_options].last].each do |color|
|
116
147
|
@ranges.to_svg.should match(/fill: #{color};/)
|
117
148
|
end
|
118
149
|
new_options = ["black", "purple", "blue", "green", "yellow", "red"]
|
119
150
|
new_range = Dates2SVG.new(@dates, :color_options => new_options).to_svg
|
120
|
-
|
151
|
+
@ranges.options[:color_options].each do |color|
|
121
152
|
new_range.should_not match(/fill: #{color};/)
|
122
153
|
end
|
123
154
|
[new_options.first, new_options.last].each do |color|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dates2svg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1.
|
4
|
+
version: 0.0.1.beta2
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -68,6 +68,7 @@ extra_rdoc_files: []
|
|
68
68
|
files:
|
69
69
|
- .gitignore
|
70
70
|
- .rspec
|
71
|
+
- .travis.yml
|
71
72
|
- Gemfile
|
72
73
|
- LICENSE.txt
|
73
74
|
- README.md
|