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.
@@ -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 (min !== null && v.y < min) {
104
- v.y = min;
105
- v.color = globalOptions.outlierColor;
106
- }
107
- if (max !== null && v.y > max) {
108
- v.y = max;
109
- return v.color = globalOptions.outlierColor;
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(s) {
114
- console.log(s.data.length);
115
- return _.each(s.data, function(v) {
116
- return this.cutoffValue(v, min, max);
117
- }, this);
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
- chartSeries = this.chart.series;
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 options;
153
- options = {
154
- chart: {
155
- renderTo: this.el,
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.timeline(sensor.interval).first
19
+ sensor.timeline_within(now - sensor.interval, now).first
20
20
  else
21
- sensor.timeline(sensor.interval * 2).first
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.timeline(time_span)
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 highcharts.js application.js bootstrap.js}.each do |jsfile|
8
- %script{:type => 'text/javascript', :src => url("/js/#{jsfile}")}
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{:rel => 'stylesheet', :href => url("/css/#{cssfile}"), :type => 'text/css', :media => 'screen'}
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
- sensors.map{ |s| s.timeline_data(tspan, show_last_point) }.flatten(1)
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
- type: type,
60
- name: values_label,
61
- data: sensors.map{|s| s.last_point_data(show_last_point)}.flatten(1)
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, highchart_options" do
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.highchart_options.should == {}
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.spline "bar_widget" do |w|
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 "#highchart_options" do
52
- it "should set highchart_options" do
53
- layout.highchart_options({b: 1})
54
- layout.to_layout.highchart_options.should == {b: 1}
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