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.
@@ -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