postrunner 0.0.2 → 0.0.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/COPYING +339 -0
- data/Rakefile +6 -0
- data/lib/postrunner/ActivitiesDB.rb +113 -37
- data/lib/postrunner/Activity.rb +55 -12
- data/lib/postrunner/ActivityReport.rb +102 -0
- data/lib/postrunner/ActivityView.rb +133 -0
- data/lib/postrunner/ChartView.rb +195 -0
- data/lib/postrunner/FlexiTable.rb +289 -0
- data/lib/postrunner/HTMLBuilder.rb +67 -0
- data/lib/postrunner/Main.rb +74 -34
- data/lib/postrunner/PersonalRecords.rb +145 -0
- data/lib/postrunner/TrackView.rb +129 -0
- data/lib/postrunner/ViewWidgets.rb +46 -0
- data/lib/postrunner/version.rb +1 -1
- data/lib/postrunner.rb +0 -1
- data/postrunner.gemspec +5 -0
- data/spec/FlexiTable_spec.rb +14 -0
- data/spec/PostRunner_spec.rb +122 -0
- metadata +72 -4
- data/lib/postrunner/RuntimeConfig.rb +0 -20
data/lib/postrunner/Activity.rb
CHANGED
@@ -1,18 +1,22 @@
|
|
1
1
|
require 'fit4ruby'
|
2
|
-
|
2
|
+
|
3
|
+
require 'postrunner/ActivityReport'
|
4
|
+
require 'postrunner/ActivityView'
|
3
5
|
|
4
6
|
module PostRunner
|
5
7
|
|
6
8
|
class Activity
|
7
9
|
|
8
|
-
attr_reader :fit_file
|
9
|
-
attr_accessor :
|
10
|
+
attr_reader :fit_file, :name, :fit_activity
|
11
|
+
attr_accessor :db
|
10
12
|
|
11
13
|
# This is a list of variables that provide data from the fit file. To
|
12
14
|
# speed up access to it, we cache the data in the activity database.
|
13
|
-
@@CachedVariables = %w(
|
15
|
+
@@CachedVariables = %w( timestamp total_distance total_timer_time
|
16
|
+
avg_speed )
|
14
17
|
|
15
|
-
def initialize(fit_file, fit_activity, name = nil)
|
18
|
+
def initialize(db, fit_file, fit_activity, name = nil)
|
19
|
+
@db = db
|
16
20
|
@fit_file = fit_file
|
17
21
|
@fit_activity = fit_activity
|
18
22
|
@name = name || fit_file
|
@@ -24,6 +28,15 @@ module PostRunner
|
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
31
|
+
def check
|
32
|
+
@fit_activity = load_fit_file
|
33
|
+
Log.info "FIT file #{@fit_file} is OK"
|
34
|
+
end
|
35
|
+
|
36
|
+
def dump(filter)
|
37
|
+
@fit_activity = load_fit_file(filter)
|
38
|
+
end
|
39
|
+
|
27
40
|
def yaml_initialize(tag, value)
|
28
41
|
# Create attr_readers for cached variables.
|
29
42
|
@@CachedVariables.each { |v| self.class.send(:attr_reader, v.to_sym) }
|
@@ -37,7 +50,7 @@ module PostRunner
|
|
37
50
|
end
|
38
51
|
|
39
52
|
def encode_with(coder)
|
40
|
-
attr_ignore = %w( @fit_activity )
|
53
|
+
attr_ignore = %w( @db @fit_activity )
|
41
54
|
|
42
55
|
instance_variables.each do |v|
|
43
56
|
v = v.to_s
|
@@ -47,15 +60,45 @@ module PostRunner
|
|
47
60
|
end
|
48
61
|
end
|
49
62
|
|
50
|
-
def
|
51
|
-
|
63
|
+
def show
|
64
|
+
@fit_activity = load_fit_file unless @fit_activity
|
65
|
+
view = ActivityView.new(self, File.join(@db.db_dir, 'html'))
|
66
|
+
#view = TrackView.new(self, '../../html')
|
67
|
+
#view.generate_html
|
68
|
+
#chart = ChartView.new(self, '../../html')
|
69
|
+
#chart.generate_html
|
70
|
+
end
|
71
|
+
|
72
|
+
def summary
|
73
|
+
@fit_activity = load_fit_file unless @fit_activity
|
74
|
+
puts ActivityReport.new(@fit_activity).to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
def rename(name)
|
78
|
+
@name = name
|
79
|
+
end
|
80
|
+
|
81
|
+
def register_records(db)
|
82
|
+
@fit_activity.personal_records.each do |r|
|
83
|
+
if r.longest_distance == 1
|
84
|
+
# In case longest_distance is 1 the distance is stored in the
|
85
|
+
# duration field in 10-th of meters.
|
86
|
+
db.register_result(r.duration * 10.0 , 0, r.start_time, @fit_file)
|
87
|
+
else
|
88
|
+
db.register_result(r.distance, r.duration, r.start_time, @fit_file)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def load_fit_file(filter = nil)
|
96
|
+
fit_file = File.join(@db.fit_dir, @fit_file)
|
52
97
|
begin
|
53
|
-
|
98
|
+
return Fit4Ruby.read(fit_file, filter)
|
54
99
|
rescue Fit4Ruby::Error
|
55
|
-
Log.
|
56
|
-
return false
|
100
|
+
Log.fatal $!
|
57
101
|
end
|
58
|
-
@fit_activity.send(method_name, *args, &block)
|
59
102
|
end
|
60
103
|
|
61
104
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'fit4ruby'
|
2
|
+
|
3
|
+
require 'postrunner/FlexiTable'
|
4
|
+
require 'postrunner/ViewWidgets'
|
5
|
+
|
6
|
+
module PostRunner
|
7
|
+
|
8
|
+
class ActivityReport
|
9
|
+
|
10
|
+
include Fit4Ruby::Converters
|
11
|
+
include ViewWidgets
|
12
|
+
|
13
|
+
def initialize(activity)
|
14
|
+
@activity = activity
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
session = @activity.sessions[0]
|
19
|
+
|
20
|
+
summary(session).to_s + "\n" + laps.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_html(doc)
|
24
|
+
session = @activity.sessions[0]
|
25
|
+
|
26
|
+
frame(doc, 'Summary') {
|
27
|
+
summary(session).to_html(doc)
|
28
|
+
}
|
29
|
+
frame(doc, 'Laps') {
|
30
|
+
laps.to_html(doc)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def summary(session)
|
37
|
+
t = FlexiTable.new
|
38
|
+
t.enable_frame(false)
|
39
|
+
t.body
|
40
|
+
t.row([ 'Date:', session.timestamp])
|
41
|
+
t.row([ 'Distance:', "#{'%.2f' % (session.total_distance / 1000.0)} km" ])
|
42
|
+
t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
|
43
|
+
t.row([ 'Avg. Pace:',
|
44
|
+
"#{speedToPace(session.avg_speed)} min/km" ])
|
45
|
+
t.row([ 'Total Ascend:', "#{session.total_ascend} m" ])
|
46
|
+
t.row([ 'Total Descend:', "#{session.total_descent} m" ])
|
47
|
+
t.row([ 'Calories:', "#{session.total_calories} kCal" ])
|
48
|
+
t.row([ 'Avg. HR:', session.avg_heart_rate ?
|
49
|
+
"#{session.avg_heart_rate} bpm" : '-' ])
|
50
|
+
t.row([ 'Max. HR:', session.max_heart_rate ?
|
51
|
+
"#{session.max_heart_rate} bpm" : '-' ])
|
52
|
+
t.row([ 'Training Effect:', session.total_training_effect ?
|
53
|
+
session.total_training_effect : '-' ])
|
54
|
+
t.row([ 'Avg. Run Cadence:',
|
55
|
+
session.avg_running_cadence ?
|
56
|
+
"#{session.avg_running_cadence.round} spm" : '-' ])
|
57
|
+
t.row([ 'Avg. Vertical Oscillation:',
|
58
|
+
session.avg_vertical_oscillation ?
|
59
|
+
"#{'%.1f' % (session.avg_vertical_oscillation / 10)} cm" : '-' ])
|
60
|
+
t.row([ 'Avg. Ground Contact Time:',
|
61
|
+
session.avg_stance_time ?
|
62
|
+
"#{session.avg_stance_time.round} ms" : '-' ])
|
63
|
+
t.row([ 'Avg. Stride Length:',
|
64
|
+
session.avg_stride_length ?
|
65
|
+
"#{'%.2f' % (session.avg_stride_length / 2)} m" : '-' ])
|
66
|
+
rec_time = @activity.recovery_time
|
67
|
+
t.row([ 'Recovery Time:', rec_time ? secsToHMS(rec_time * 60) : '-' ])
|
68
|
+
vo2max = @activity.vo2max
|
69
|
+
t.row([ 'VO2max:', vo2max ? vo2max : '-' ])
|
70
|
+
|
71
|
+
t
|
72
|
+
end
|
73
|
+
|
74
|
+
def laps
|
75
|
+
t = FlexiTable.new
|
76
|
+
t.head
|
77
|
+
t.row([ 'Lap', 'Duration', 'Distance', 'Avg. Pace', 'Stride', 'Cadence',
|
78
|
+
'Avg. HR', 'Max. HR' ])
|
79
|
+
t.set_column_attributes(Array.new(8, { :halign => :right }))
|
80
|
+
t.body
|
81
|
+
@activity.sessions[0].laps.each.with_index do |lap, index|
|
82
|
+
t.cell(index + 1)
|
83
|
+
t.cell(secsToHMS(lap.total_timer_time))
|
84
|
+
t.cell('%.2f' % (lap.total_distance / 1000.0))
|
85
|
+
t.cell(speedToPace(lap.avg_speed))
|
86
|
+
t.cell(lap.total_strides ?
|
87
|
+
'%.2f' % (lap.total_distance / (2 * lap.total_strides)) : '')
|
88
|
+
t.cell(lap.avg_running_cadence && lap.avg_fractional_cadence ?
|
89
|
+
'%.1f' % (2 * lap.avg_running_cadence +
|
90
|
+
(2 * lap.avg_fractional_cadence) / 100.0) : '')
|
91
|
+
t.cell(lap.avg_heart_rate.to_s)
|
92
|
+
t.cell(lap.max_heart_rate.to_s)
|
93
|
+
t.new_row
|
94
|
+
end
|
95
|
+
|
96
|
+
t
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'fit4ruby'
|
2
|
+
|
3
|
+
require 'postrunner/HTMLBuilder'
|
4
|
+
require 'postrunner/ActivityReport'
|
5
|
+
require 'postrunner/ViewWidgets'
|
6
|
+
require 'postrunner/TrackView'
|
7
|
+
require 'postrunner/ChartView'
|
8
|
+
|
9
|
+
module PostRunner
|
10
|
+
|
11
|
+
class ActivityView
|
12
|
+
|
13
|
+
include ViewWidgets
|
14
|
+
|
15
|
+
def initialize(activity, output_dir)
|
16
|
+
@activity = activity
|
17
|
+
@output_dir = output_dir
|
18
|
+
@output_file = nil
|
19
|
+
|
20
|
+
ensure_output_dir
|
21
|
+
|
22
|
+
@doc = HTMLBuilder.new
|
23
|
+
generate_html(@doc)
|
24
|
+
write_file
|
25
|
+
show_in_browser
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def ensure_output_dir
|
31
|
+
unless Dir.exists?(@output_dir)
|
32
|
+
begin
|
33
|
+
Dir.mkdir(@output_dir)
|
34
|
+
rescue SystemCallError
|
35
|
+
Log.fatal "Cannot create output directory '#{@output_dir}': #{$!}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_html(doc)
|
41
|
+
@report = ActivityReport.new(@activity.fit_activity)
|
42
|
+
@track_view = TrackView.new(@activity)
|
43
|
+
@chart_view = ChartView.new(@activity)
|
44
|
+
|
45
|
+
doc.html {
|
46
|
+
head(doc)
|
47
|
+
body(doc)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def head(doc)
|
52
|
+
doc.head {
|
53
|
+
doc.meta({ 'http-equiv' => 'Content-Type',
|
54
|
+
'content' => 'text/html; charset=utf-8' })
|
55
|
+
doc.meta({ 'name' => 'viewport',
|
56
|
+
'content' => 'width=device-width, ' +
|
57
|
+
'initial-scale=1.0, maximum-scale=1.0, ' +
|
58
|
+
'user-scalable=0' })
|
59
|
+
doc.title("PostRunner Activity: #{@activity.name}")
|
60
|
+
style(doc)
|
61
|
+
view_widgets_style(doc)
|
62
|
+
@chart_view.head(doc)
|
63
|
+
@track_view.head(doc)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def style(doc)
|
68
|
+
doc.style(<<EOT
|
69
|
+
.main {
|
70
|
+
width: 1210px;
|
71
|
+
margin: 0 auto;
|
72
|
+
}
|
73
|
+
.left_col {
|
74
|
+
float: left;
|
75
|
+
width: 400px;
|
76
|
+
}
|
77
|
+
.right_col {
|
78
|
+
float: right;
|
79
|
+
width: 600px;
|
80
|
+
}
|
81
|
+
.widget_container {
|
82
|
+
box-sizing: border-box;
|
83
|
+
width: 600px;
|
84
|
+
padding: 10px 15px 15px 15px;
|
85
|
+
margin: 15px auto 15px auto;
|
86
|
+
border: 1px solid #ddd;
|
87
|
+
background: #fff;
|
88
|
+
background: linear-gradient(#f6f6f6 0, #fff 50px);
|
89
|
+
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
|
90
|
+
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
|
91
|
+
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
|
92
|
+
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
|
93
|
+
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
|
94
|
+
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
95
|
+
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
96
|
+
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
97
|
+
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
98
|
+
}
|
99
|
+
EOT
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def body(doc)
|
104
|
+
doc.body({ 'onload' => 'init()' }) {
|
105
|
+
doc.div({ 'class' => 'main' }) {
|
106
|
+
doc.div({ 'class' => 'left_col' }) {
|
107
|
+
@report.to_html(doc)
|
108
|
+
@track_view.div(doc)
|
109
|
+
}
|
110
|
+
doc.div({ 'class' => 'right_col' }) {
|
111
|
+
@chart_view.div(doc)
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def write_file
|
118
|
+
@output_file = File.join(@output_dir, "#{@activity.fit_file[0..-5]}.html")
|
119
|
+
begin
|
120
|
+
File.write(@output_file, @doc.to_html)
|
121
|
+
rescue IOError
|
122
|
+
Log.fatal "Cannot write activity view file '#{@output_file}: #{$!}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def show_in_browser
|
127
|
+
system("firefox \"#{@output_file}\" &")
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'postrunner/ViewWidgets'
|
2
|
+
|
3
|
+
module PostRunner
|
4
|
+
|
5
|
+
class ChartView
|
6
|
+
|
7
|
+
include ViewWidgets
|
8
|
+
|
9
|
+
def initialize(activity)
|
10
|
+
@activity = activity
|
11
|
+
end
|
12
|
+
|
13
|
+
def head(doc)
|
14
|
+
[ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
|
15
|
+
'flot/jquery.flot.time.js' ].each do |js|
|
16
|
+
doc.script({ 'language' => 'javascript', 'type' => 'text/javascript',
|
17
|
+
'src' => js })
|
18
|
+
end
|
19
|
+
doc.style(style)
|
20
|
+
doc.script(java_script)
|
21
|
+
end
|
22
|
+
|
23
|
+
def div(doc)
|
24
|
+
chart_div(doc, 'pace', 'Pace (min/km)')
|
25
|
+
chart_div(doc, 'altitude', 'Elevation (m)')
|
26
|
+
chart_div(doc, 'heart_rate', 'Heart Rate (bpm)')
|
27
|
+
chart_div(doc, 'cadence', 'Run Cadence (spm)')
|
28
|
+
chart_div(doc, 'vertical_oscillation', 'Vertical Oscillation (cm)')
|
29
|
+
chart_div(doc, 'stance_time', 'Ground Contact Time (ms)')
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def style
|
35
|
+
<<EOT
|
36
|
+
.chart-container {
|
37
|
+
box-sizing: border-box;
|
38
|
+
width: 600px;
|
39
|
+
height: 200px;
|
40
|
+
padding: 10px 15px 15px 15px;
|
41
|
+
margin: 15px auto 15px auto;
|
42
|
+
border: 1px solid #ddd;
|
43
|
+
background: #fff;
|
44
|
+
background: linear-gradient(#f6f6f6 0, #fff 50px);
|
45
|
+
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
|
46
|
+
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
|
47
|
+
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
|
48
|
+
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
|
49
|
+
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
|
50
|
+
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
51
|
+
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
52
|
+
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
53
|
+
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
54
|
+
}
|
55
|
+
.chart-placeholder {
|
56
|
+
width: 570px;
|
57
|
+
height: 200px;
|
58
|
+
font-size: 14px;
|
59
|
+
line-height: 1.2em;
|
60
|
+
}
|
61
|
+
EOT
|
62
|
+
end
|
63
|
+
|
64
|
+
def java_script
|
65
|
+
s = "$(function() {\n"
|
66
|
+
|
67
|
+
s << line_graph('pace', '#0A7BEE' )
|
68
|
+
s << line_graph('altitude', '#5AAA44')
|
69
|
+
s << line_graph('heart_rate', '#900000')
|
70
|
+
s << point_graph('cadence',
|
71
|
+
[ [ '#EE3F2D', 151 ],
|
72
|
+
[ '#F79666', 163 ],
|
73
|
+
[ '#A0D488', 174 ],
|
74
|
+
[ '#96D7DE', 185 ],
|
75
|
+
[ '#A88BBB', nil ] ], 2)
|
76
|
+
s << point_graph('vertical_oscillation',
|
77
|
+
[ [ '#A88BBB', 6.7 ],
|
78
|
+
[ '#96D7DE', 8.4 ],
|
79
|
+
[ '#A0D488', 10.1 ],
|
80
|
+
[ '#F79666', 11.8 ],
|
81
|
+
[ '#EE3F2D', nil ] ], 0.1)
|
82
|
+
s << point_graph('stance_time',
|
83
|
+
[ [ '#A88BBB', 208 ],
|
84
|
+
[ '#96D7DE', 241 ],
|
85
|
+
[ '#A0D488', 273 ],
|
86
|
+
[ '#F79666', 305 ],
|
87
|
+
[ '#EE3F2D', nil ] ])
|
88
|
+
|
89
|
+
s << "\n});\n"
|
90
|
+
|
91
|
+
s
|
92
|
+
end
|
93
|
+
|
94
|
+
def line_graph(field, color = nil)
|
95
|
+
s = "var #{field}_data = [\n"
|
96
|
+
|
97
|
+
first = true
|
98
|
+
start_time = @activity.fit_activity.sessions[0].start_time.to_i
|
99
|
+
@activity.fit_activity.records.each do |r|
|
100
|
+
if first
|
101
|
+
first = false
|
102
|
+
else
|
103
|
+
s << ', '
|
104
|
+
end
|
105
|
+
value = r.send(field)
|
106
|
+
if field == 'pace'
|
107
|
+
if value > 20.0
|
108
|
+
value = nil
|
109
|
+
else
|
110
|
+
value = (value * 3600.0 * 1000).to_i
|
111
|
+
end
|
112
|
+
end
|
113
|
+
s << "[ #{((r.timestamp.to_i - start_time) * 1000).to_i}, " +
|
114
|
+
"#{value ? value : 'null'} ]"
|
115
|
+
end
|
116
|
+
|
117
|
+
s << <<"EOT"
|
118
|
+
];
|
119
|
+
|
120
|
+
$.plot("##{field}_chart",
|
121
|
+
[ { data: #{field}_data,
|
122
|
+
#{color ? "color: \"#{color}\"," : ''}
|
123
|
+
lines: { show: true#{field == 'pace' ? '' : ', fill: true'} } } ],
|
124
|
+
{ xaxis: { mode: "time" }
|
125
|
+
EOT
|
126
|
+
if field == 'pace'
|
127
|
+
s << ", yaxis: { mode: \"time\",\n" +
|
128
|
+
" transform: function (v) { return -v; },\n" +
|
129
|
+
" inverseTransform: function (v) { return -v; } }"
|
130
|
+
end
|
131
|
+
s << "});\n"
|
132
|
+
end
|
133
|
+
|
134
|
+
def point_graph(field, colors, multiplier = 1)
|
135
|
+
# We need to split the field values into separate data sets for each
|
136
|
+
# color. The max value for each color determines which set a data point
|
137
|
+
# ends up in.
|
138
|
+
# Initialize the data sets. The key for data_sets is the corresponding
|
139
|
+
# index in colors.
|
140
|
+
data_sets = {}
|
141
|
+
colors.each.with_index { |cp, i| data_sets[i] = [] }
|
142
|
+
|
143
|
+
# Now we can split the field values into the sets.
|
144
|
+
start_time = @activity.fit_activity.sessions[0].start_time.to_i
|
145
|
+
@activity.fit_activity.records.each do |r|
|
146
|
+
# Undefined values will be discarded.
|
147
|
+
next unless (value = r.instance_variable_get('@' + field))
|
148
|
+
value *= multiplier
|
149
|
+
|
150
|
+
# Find the right set by looking at the maximum allowed values for each
|
151
|
+
# color.
|
152
|
+
colors.each.with_index do |col_max_value, i|
|
153
|
+
col, max_value = col_max_value
|
154
|
+
if max_value.nil? || value < max_value
|
155
|
+
# A max_value of nil means all values allowed. The value is in the
|
156
|
+
# allowed range for this set, so add the value as x/y pair to the
|
157
|
+
# set.
|
158
|
+
x_val = (r.timestamp.to_i - start_time) * 1000
|
159
|
+
data_sets[i] << [ x_val, value ]
|
160
|
+
# Abort the color loop since we've found the right set already.
|
161
|
+
break
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Now generate the JS variable definitions for each set.
|
167
|
+
s = ''
|
168
|
+
data_sets.each do |index, ds|
|
169
|
+
s << "var #{field}_data_#{index} = [\n"
|
170
|
+
s << ds.map { |dp| "[ #{dp[0]}, #{dp[1]} ]" }.join(', ')
|
171
|
+
s << " ];\n"
|
172
|
+
end
|
173
|
+
|
174
|
+
s << "$.plot(\"##{field}_chart\", [\n"
|
175
|
+
s << data_sets.map do |index, ds|
|
176
|
+
"{ data: #{field}_data_#{index},\n" +
|
177
|
+
" color: \"#{colors[index][0]}\",\n" +
|
178
|
+
" points: { show: true, fillColor: \"#{colors[index][0]}\", " +
|
179
|
+
" fill: true, radius: 2 } }"
|
180
|
+
end.join(', ')
|
181
|
+
s << "], { xaxis: { mode: \"time\" } });\n"
|
182
|
+
|
183
|
+
s
|
184
|
+
end
|
185
|
+
|
186
|
+
def chart_div(doc, field, title)
|
187
|
+
frame(doc, title) {
|
188
|
+
doc.div({ 'id' => "#{field}_chart", 'class' => 'chart-placeholder'})
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|