pulse-meter 0.1.11 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|