pulse-meter 0.1.11 → 0.2.0
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.
- data/README.md +9 -18
- data/Rakefile +1 -1
- data/examples/basic.ru +20 -12
- data/examples/full/server.ru +9 -18
- data/examples/minimal/client.rb +2 -1
- data/lib/pulse-meter/version.rb +1 -1
- data/lib/pulse-meter/visualize/dsl/layout.rb +11 -10
- data/lib/pulse-meter/visualize/dsl/page.rb +15 -6
- data/lib/pulse-meter/visualize/layout.rb +4 -7
- data/lib/pulse-meter/visualize/page.rb +2 -2
- data/lib/pulse-meter/visualize/public/js/application.coffee +109 -67
- data/lib/pulse-meter/visualize/public/js/application.js +114 -69
- data/lib/pulse-meter/visualize/sensor.rb +11 -7
- data/lib/pulse-meter/visualize/series_extractor.rb +4 -4
- data/lib/pulse-meter/visualize/views/main.haml +16 -4
- data/lib/pulse-meter/visualize/widget.rb +64 -8
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +7 -7
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +2 -2
- data/spec/pulse_meter/visualize/layout_spec.rb +7 -8
- data/spec/pulse_meter/visualize/page_spec.rb +55 -85
- data/spec/pulse_meter/visualize/sensor_spec.rb +17 -17
- data/spec/pulse_meter/visualize/series_extractor_spec.rb +3 -3
- data/spec/pulse_meter/visualize/widget_spec.rb +26 -35
- data/spec/pulse_meter/visualizer_spec.rb +2 -2
- metadata +3 -4
- data/lib/pulse-meter/visualize/public/js/highcharts.js +0 -203
@@ -4,11 +4,6 @@
|
|
4
4
|
$(function() {
|
5
5
|
var AppRouter, PageInfo, PageInfoList, PageTitleView, PageTitlesView, Widget, WidgetChartView, WidgetList, WidgetListView, WidgetView, appRouter, globalOptions, pageInfos, pageTitlesApp, widgetList, widgetListApp;
|
6
6
|
globalOptions = gon.options;
|
7
|
-
Highcharts.setOptions({
|
8
|
-
global: {
|
9
|
-
useUTC: globalOptions.useUtc
|
10
|
-
}
|
11
|
-
});
|
12
7
|
PageInfo = Backbone.Model.extend({});
|
13
8
|
PageInfoList = Backbone.Collection.extend({
|
14
9
|
model: PageInfo,
|
@@ -100,21 +95,26 @@
|
|
100
95
|
}
|
101
96
|
},
|
102
97
|
cutoffValue: function(v, min, max) {
|
103
|
-
if (
|
104
|
-
v
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
98
|
+
if (v !== null) {
|
99
|
+
if (min !== null && v < min) {
|
100
|
+
return min;
|
101
|
+
} else if (max !== null && v > max) {
|
102
|
+
return max;
|
103
|
+
} else {
|
104
|
+
return v;
|
105
|
+
}
|
106
|
+
} else {
|
107
|
+
return 0;
|
110
108
|
}
|
111
109
|
},
|
112
110
|
cutoff: function(min, max) {
|
113
|
-
return _.each(this.get('series'), function(
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
111
|
+
return _.each(this.get('series').rows, function(row) {
|
112
|
+
var i, _i, _ref, _results;
|
113
|
+
_results = [];
|
114
|
+
for (i = _i = 1, _ref = row.length - 1; 1 <= _ref ? _i <= _ref : _i >= _ref; i = 1 <= _ref ? ++_i : --_i) {
|
115
|
+
_results.push(row[i] = this.cutoffValue(row[i], min, max));
|
116
|
+
}
|
117
|
+
return _results;
|
118
118
|
}, this);
|
119
119
|
},
|
120
120
|
forceUpdate: function() {
|
@@ -123,6 +123,98 @@
|
|
123
123
|
return model.trigger('redraw');
|
124
124
|
}
|
125
125
|
});
|
126
|
+
},
|
127
|
+
pieData: function() {
|
128
|
+
var data;
|
129
|
+
data = new google.visualization.DataTable();
|
130
|
+
data.addColumn('string', 'Title');
|
131
|
+
data.addColumn('number', this.get('valuesTitle'));
|
132
|
+
data.addRows(this.get('series').data);
|
133
|
+
return data;
|
134
|
+
},
|
135
|
+
dateOffset: function() {
|
136
|
+
if (globalOptions.useUtc) {
|
137
|
+
return (new Date).getTimezoneOffset() * 60000;
|
138
|
+
} else {
|
139
|
+
return 0;
|
140
|
+
}
|
141
|
+
},
|
142
|
+
lineData: function() {
|
143
|
+
var data, dateOffset, series, title;
|
144
|
+
title = this.get('title');
|
145
|
+
data = new google.visualization.DataTable();
|
146
|
+
data.addColumn('datetime', 'Time');
|
147
|
+
dateOffset = this.dateOffset();
|
148
|
+
series = this.get('series');
|
149
|
+
_.each(series.titles, function(t) {
|
150
|
+
return data.addColumn('number', t);
|
151
|
+
});
|
152
|
+
console.log(series);
|
153
|
+
_.each(series.rows, function(row) {
|
154
|
+
row[0] = new Date(row[0] + dateOffset);
|
155
|
+
return data.addRow(row);
|
156
|
+
});
|
157
|
+
return data;
|
158
|
+
},
|
159
|
+
options: function() {
|
160
|
+
return {
|
161
|
+
title: this.get('title'),
|
162
|
+
lineWidth: 1,
|
163
|
+
chartArea: {
|
164
|
+
left: 40,
|
165
|
+
width: '100%'
|
166
|
+
},
|
167
|
+
height: 300,
|
168
|
+
legend: {
|
169
|
+
position: 'bottom'
|
170
|
+
},
|
171
|
+
vAxis: {
|
172
|
+
title: this.get('valuesTitle')
|
173
|
+
},
|
174
|
+
axisTitlesPosition: 'in'
|
175
|
+
};
|
176
|
+
},
|
177
|
+
pieOptions: function() {
|
178
|
+
return $.extend(true, this.options(), {
|
179
|
+
slices: this.get('series').options
|
180
|
+
});
|
181
|
+
},
|
182
|
+
lineOptions: function() {
|
183
|
+
return $.extend(true, this.options(), {
|
184
|
+
hAxis: {
|
185
|
+
format: 'yyyy.MM.dd HH:mm:ss'
|
186
|
+
},
|
187
|
+
series: this.get('series').options
|
188
|
+
});
|
189
|
+
},
|
190
|
+
tableOptions: function() {
|
191
|
+
return $.extend(true, this.lineOptions(), {
|
192
|
+
sortColumn: 0,
|
193
|
+
sortAscending: false
|
194
|
+
});
|
195
|
+
},
|
196
|
+
chartOptions: function() {
|
197
|
+
var opts;
|
198
|
+
opts = this.get('type') === 'pie' ? this.pieOptions() : this.get('type') === 'table' ? this.tableOptions() : this.lineOptions();
|
199
|
+
return $.extend(true, opts, globalOptions.gchartOptions, pageInfos.selected().get('gchartOptions'));
|
200
|
+
},
|
201
|
+
chartData: function() {
|
202
|
+
if (this.get('type') === 'pie') {
|
203
|
+
return this.pieData();
|
204
|
+
} else {
|
205
|
+
return this.lineData();
|
206
|
+
}
|
207
|
+
},
|
208
|
+
chartClass: function() {
|
209
|
+
if (this.get('type') === 'pie') {
|
210
|
+
return google.visualization.PieChart;
|
211
|
+
} else if (this.get('type') === 'area') {
|
212
|
+
return google.visualization.AreaChart;
|
213
|
+
} else if (this.get('type') === 'table') {
|
214
|
+
return google.visualization.Table;
|
215
|
+
} else {
|
216
|
+
return google.visualization.LineChart;
|
217
|
+
}
|
126
218
|
}
|
127
219
|
});
|
128
220
|
WidgetList = Backbone.Collection.extend({
|
@@ -137,61 +229,14 @@
|
|
137
229
|
return this.model.bind('destroy', this.remove, this);
|
138
230
|
},
|
139
231
|
updateData: function(min, max) {
|
140
|
-
var chartSeries, i, newSeries, _i, _ref;
|
141
232
|
this.model.cutoff(min, max);
|
142
|
-
|
143
|
-
newSeries = this.model.get('series');
|
144
|
-
for (i = _i = 0, _ref = chartSeries.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
|
145
|
-
if (newSeries[i] != null) {
|
146
|
-
chartSeries[i].setData(newSeries[i].data, false);
|
147
|
-
}
|
148
|
-
}
|
149
|
-
return this.chart.redraw();
|
233
|
+
return this.chart.draw(this.model.chartData(), this.model.chartOptions());
|
150
234
|
},
|
151
235
|
render: function() {
|
152
|
-
var
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
plotBorderWidth: 1,
|
157
|
-
spacingLeft: 0,
|
158
|
-
spacingRight: 0,
|
159
|
-
type: this.model.get('type'),
|
160
|
-
animation: false,
|
161
|
-
zoomType: 'x'
|
162
|
-
},
|
163
|
-
credits: {
|
164
|
-
enabled: false
|
165
|
-
},
|
166
|
-
title: {
|
167
|
-
text: this.model.get('title')
|
168
|
-
},
|
169
|
-
xAxis: {
|
170
|
-
type: 'datetime'
|
171
|
-
},
|
172
|
-
yAxis: {
|
173
|
-
title: {
|
174
|
-
text: this.model.get('valuesTitle')
|
175
|
-
}
|
176
|
-
},
|
177
|
-
tooltip: {
|
178
|
-
xDateFormat: '%Y-%m-%d %H:%M:%S',
|
179
|
-
valueDecimals: 6
|
180
|
-
},
|
181
|
-
series: this.model.get('series'),
|
182
|
-
plotOptions: {
|
183
|
-
series: {
|
184
|
-
animation: false,
|
185
|
-
lineWidth: 1,
|
186
|
-
shadow: false,
|
187
|
-
marker: {
|
188
|
-
radius: 0
|
189
|
-
}
|
190
|
-
}
|
191
|
-
}
|
192
|
-
};
|
193
|
-
$.extend(true, options, globalOptions.highchartOptions, pageInfos.selected().get('highchartOptions'));
|
194
|
-
return this.chart = new Highcharts.Chart(options);
|
236
|
+
var chartClass;
|
237
|
+
chartClass = this.model.chartClass();
|
238
|
+
this.chart = new chartClass(this.el);
|
239
|
+
return this.updateData();
|
195
240
|
}
|
196
241
|
});
|
197
242
|
WidgetView = Backbone.View.extend({
|
@@ -12,13 +12,13 @@ module PulseMeter
|
|
12
12
|
@color = args[:color]
|
13
13
|
end
|
14
14
|
|
15
|
-
def last_value(need_incomplete=false)
|
15
|
+
def last_value(now, need_incomplete=false)
|
16
16
|
sensor = real_sensor
|
17
17
|
|
18
18
|
sensor_data = if need_incomplete
|
19
|
-
sensor.
|
19
|
+
sensor.timeline_within(now - sensor.interval, now).first
|
20
20
|
else
|
21
|
-
sensor.
|
21
|
+
sensor.timeline_within(now - sensor.interval * 2, now).first
|
22
22
|
end
|
23
23
|
|
24
24
|
if sensor_data.is_a?(PulseMeter::SensorData)
|
@@ -28,13 +28,13 @@ module PulseMeter
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def last_point_data(need_incomplete=false)
|
32
|
-
extractor.point_data(last_value(need_incomplete))
|
31
|
+
def last_point_data(now, need_incomplete=false)
|
32
|
+
extractor.point_data(last_value(now, need_incomplete))
|
33
33
|
end
|
34
34
|
|
35
|
-
def timeline_data(time_span, need_incomplete = false)
|
35
|
+
def timeline_data(now, time_span, need_incomplete = false)
|
36
36
|
sensor = real_sensor
|
37
|
-
timeline_data = sensor.
|
37
|
+
timeline_data = sensor.timeline_within(now - time_span, now)
|
38
38
|
timeline_data.pop unless need_incomplete
|
39
39
|
extractor.series_data(timeline_data)
|
40
40
|
end
|
@@ -47,6 +47,10 @@ module PulseMeter
|
|
47
47
|
real_sensor.class
|
48
48
|
end
|
49
49
|
|
50
|
+
def interval
|
51
|
+
real_sensor.interval
|
52
|
+
end
|
53
|
+
|
50
54
|
def extractor
|
51
55
|
PulseMeter::Visualize.extractor(self)
|
52
56
|
end
|
@@ -14,15 +14,15 @@ module PulseMeter
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def series_data(timeline_data)
|
17
|
-
{
|
17
|
+
[{
|
18
18
|
data: timeline_data.map{|sd| {x: sd.start_time.to_i*1000, y: to_float(sd.value)}}
|
19
|
-
}.merge(opts_to_add)
|
19
|
+
}.merge(opts_to_add)]
|
20
20
|
end
|
21
21
|
|
22
22
|
def point_data(value)
|
23
|
-
{
|
23
|
+
[{
|
24
24
|
y: to_float(value)
|
25
|
-
}.merge(opts_to_add)
|
25
|
+
}.merge(opts_to_add)]
|
26
26
|
end
|
27
27
|
|
28
28
|
protected
|
@@ -4,11 +4,13 @@
|
|
4
4
|
:javascript
|
5
5
|
var ROOT = "#{url('/')}";
|
6
6
|
= include_gon
|
7
|
-
- %w{jquery-1.7.2.min.js json2.js underscore-min.js backbone-min.js
|
8
|
-
%script{:
|
7
|
+
- %w{jquery-1.7.2.min.js json2.js underscore-min.js backbone-min.js application.js bootstrap.js}.each do |jsfile|
|
8
|
+
%script{type: 'text/javascript', src: url("/js/#{jsfile}")}
|
9
9
|
- %w{bootstrap.min.css application.css}.each do |cssfile|
|
10
|
-
%link{:
|
11
|
-
|
10
|
+
%link{rel: 'stylesheet', href: url("/css/#{cssfile}"), type: 'text/css', media: 'screen'}
|
11
|
+
%script{type: 'text/javascript', src: "https://www.google.com/jsapi"}
|
12
|
+
:javascript
|
13
|
+
google.load("visualization", "1", {packages:["corechart", "table"]});
|
12
14
|
%body
|
13
15
|
%script#widget-template{type: 'text/template'}
|
14
16
|
#plotarea
|
@@ -17,6 +19,7 @@
|
|
17
19
|
%button#refresh.btn.btn-mini
|
18
20
|
%i.icon-refresh
|
19
21
|
Refresh
|
22
|
+
<% if(type != 'table'){ %>
|
20
23
|
%span.space
|
21
24
|
%label
|
22
25
|
Cutoff min:
|
@@ -25,6 +28,7 @@
|
|
25
28
|
%label
|
26
29
|
Cutoff max:
|
27
30
|
%input.btn-mini#cutoff-max
|
31
|
+
<% } %>
|
28
32
|
%span.space
|
29
33
|
%label
|
30
34
|
Refresh:
|
@@ -44,10 +48,18 @@
|
|
44
48
|
%option{value: 60 * 60 * 24 * 7 * 2} 2 weeks
|
45
49
|
%option{value: 60 * 60 * 24 * 30} 1 month
|
46
50
|
%button#extend-timespan.btn.btn-mini
|
51
|
+
<% if(type != 'table'){ %>
|
47
52
|
%i.icon-arrow-left
|
53
|
+
<% } else { %>
|
54
|
+
%i.icon-arrow-down
|
55
|
+
<% } %>
|
48
56
|
Extend
|
49
57
|
%button#reset-timespan.btn.btn-mini
|
58
|
+
<% if(type != 'table'){ %>
|
50
59
|
%i.icon-arrow-right
|
60
|
+
<% } else { %>
|
61
|
+
%i.icon-arrow-up
|
62
|
+
<% } %>
|
51
63
|
Reset
|
52
64
|
<% } %>
|
53
65
|
%hr
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module PulseMeter
|
2
2
|
module Visualize
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
3
5
|
class Widget
|
6
|
+
class NotATimelinedSensorInWidget < PulseMeter::Visualize::Error; end
|
7
|
+
class DifferentSensorIntervalsInWidget < PulseMeter::Visualize::Error; end
|
8
|
+
|
4
9
|
attr_reader :sensors
|
5
10
|
attr_reader :title
|
6
11
|
attr_reader :type
|
@@ -23,6 +28,7 @@ module PulseMeter
|
|
23
28
|
end
|
24
29
|
|
25
30
|
def data(options = {})
|
31
|
+
ensure_sensor_match!
|
26
32
|
real_timespan = options[:timespan] || timespan
|
27
33
|
{
|
28
34
|
title: title,
|
@@ -39,27 +45,77 @@ module PulseMeter
|
|
39
45
|
|
40
46
|
def series_data(tspan)
|
41
47
|
case type
|
42
|
-
when :spline
|
43
|
-
line_series_data(tspan)
|
44
48
|
when :line
|
45
49
|
line_series_data(tspan)
|
46
50
|
when :area
|
47
51
|
line_series_data(tspan)
|
48
52
|
when :pie
|
49
53
|
pie_series_data
|
54
|
+
when :table
|
55
|
+
line_series_data(tspan)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def ensure_sensor_match!
|
60
|
+
intervals = []
|
61
|
+
sensors.each do |s|
|
62
|
+
unless s.type < PulseMeter::Sensor::Timeline
|
63
|
+
raise NotATimelinedSensorInWidget, "sensor `#{s.name}' is not timelined"
|
64
|
+
end
|
65
|
+
intervals << s.interval
|
66
|
+
end
|
67
|
+
|
68
|
+
unless intervals.all?{|i| i == intervals.first}
|
69
|
+
interval_notice = sensors.map{|s| "#{s.name}: #{s.interval}"}.join(', ')
|
70
|
+
raise DifferentSensorIntervalsInWidget, "Sensors with different intervals in a single widget: #{interval_notice}"
|
50
71
|
end
|
51
72
|
end
|
52
73
|
|
53
74
|
def line_series_data(tspan)
|
54
|
-
|
75
|
+
now = Time.now
|
76
|
+
sensor_datas = sensors.map{ |s|
|
77
|
+
s.timeline_data(now, tspan, show_last_point)
|
78
|
+
}
|
79
|
+
rows = []
|
80
|
+
titles = []
|
81
|
+
series_options = []
|
82
|
+
datas = []
|
83
|
+
sensor_datas.each do |sensor_data|
|
84
|
+
sensor_data.each do |tl|
|
85
|
+
titles << tl[:name]
|
86
|
+
series_options << {color: tl[:color]}
|
87
|
+
datas << tl[:data]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
unless datas.empty?
|
91
|
+
first = datas.shift
|
92
|
+
first.each_with_index do |tl_data, row_num|
|
93
|
+
rows << datas.each_with_object([tl_data[:x], tl_data[:y]]) do |data_col, row|
|
94
|
+
row << data_col[row_num][:y]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
{
|
99
|
+
titles: titles,
|
100
|
+
rows: rows,
|
101
|
+
options: series_options
|
102
|
+
}
|
55
103
|
end
|
56
104
|
|
57
105
|
def pie_series_data
|
58
|
-
[
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
106
|
+
values = []
|
107
|
+
slice_options = []
|
108
|
+
now = Time.now
|
109
|
+
sensors.each do |s|
|
110
|
+
s.last_point_data(now, show_last_point).each do |point_data|
|
111
|
+
values << [point_data[:name], point_data[:y]]
|
112
|
+
slice_options << {color: point_data[:color]}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
{
|
116
|
+
data: values,
|
117
|
+
options: slice_options
|
118
|
+
}
|
63
119
|
end
|
64
120
|
|
65
121
|
end
|
@@ -7,12 +7,12 @@ describe PulseMeter::Visualize::DSL::Layout do
|
|
7
7
|
let(:layout){ described_class.new }
|
8
8
|
|
9
9
|
describe '.new' do
|
10
|
-
it "should initialize pages, title, use_utc,
|
10
|
+
it "should initialize pages, title, use_utc, gchart_options" do
|
11
11
|
l = layout.to_layout
|
12
12
|
l.title.should == PulseMeter::Visualize::DSL::Layout::DEFAULT_TITLE
|
13
13
|
l.pages.should == []
|
14
14
|
l.use_utc.should be_true
|
15
|
-
l.
|
15
|
+
l.gchart_options.should == {}
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -20,7 +20,7 @@ describe PulseMeter::Visualize::DSL::Layout do
|
|
20
20
|
it "should add page constructed by block to pages" do
|
21
21
|
layout.page "My Foo Page" do |p|
|
22
22
|
p.pie "foo_widget", sensor: sensor_name
|
23
|
-
p.
|
23
|
+
p.line "bar_widget" do |w|
|
24
24
|
w.sensor(sensor_name)
|
25
25
|
end
|
26
26
|
end
|
@@ -48,10 +48,10 @@ describe PulseMeter::Visualize::DSL::Layout do
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
describe "#
|
52
|
-
it "should set
|
53
|
-
layout.
|
54
|
-
layout.to_layout.
|
51
|
+
describe "#gchart_options" do
|
52
|
+
it "should set gchart_options" do
|
53
|
+
layout.gchart_options({b: 1})
|
54
|
+
layout.to_layout.gchart_options.should == {b: 1}
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|