blackbeard 0.0.4.0 → 0.0.5.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.
- checksums.yaml +4 -4
- data/README.md +44 -16
- data/TODO.md +3 -1
- data/dashboard/routes/cohorts.rb +25 -0
- data/dashboard/routes/features.rb +7 -4
- data/dashboard/routes/groups.rb +6 -3
- data/dashboard/routes/metrics.rb +25 -5
- data/dashboard/routes/tests.rb +6 -3
- data/dashboard/views/cohorts/index.erb +22 -0
- data/dashboard/views/cohorts/show.erb +41 -0
- data/dashboard/views/groups/show.erb +2 -2
- data/dashboard/views/layout.erb +1 -0
- data/dashboard/views/metrics/show.erb +37 -10
- data/dashboard/views/shared/_charts.erb +20 -0
- data/lib/blackbeard/chart.rb +65 -0
- data/lib/blackbeard/chartable.rb +47 -0
- data/lib/blackbeard/cohort.rb +48 -0
- data/lib/blackbeard/cohort_data.rb +72 -0
- data/lib/blackbeard/cohort_metric.rb +47 -0
- data/lib/blackbeard/context.rb +11 -2
- data/lib/blackbeard/dashboard.rb +2 -3
- data/lib/blackbeard/dashboard_helpers.rb +0 -22
- data/lib/blackbeard/errors.rb +2 -0
- data/lib/blackbeard/group.rb +3 -0
- data/lib/blackbeard/group_metric.rb +39 -0
- data/lib/blackbeard/metric.rb +30 -14
- data/lib/blackbeard/metric_data/base.rb +18 -35
- data/lib/blackbeard/metric_data/total.rb +2 -1
- data/lib/blackbeard/metric_data/uid_generator.rb +38 -0
- data/lib/blackbeard/metric_data/unique.rb +1 -1
- data/lib/blackbeard/metric_date.rb +10 -0
- data/lib/blackbeard/metric_hour.rb +8 -0
- data/lib/blackbeard/pirate.rb +18 -0
- data/lib/blackbeard/redis_store.rb +10 -1
- data/lib/blackbeard/storable.rb +1 -0
- data/lib/blackbeard/version.rb +1 -1
- data/spec/chart_spec.rb +38 -0
- data/spec/chartable_spec.rb +56 -0
- data/spec/cohort_data_spec.rb +142 -0
- data/spec/cohort_metric_spec.rb +26 -0
- data/spec/cohort_spec.rb +31 -0
- data/spec/context_spec.rb +9 -1
- data/spec/dashboard/cohorts_spec.rb +43 -0
- data/spec/dashboard/groups_spec.rb +0 -7
- data/spec/dashboard/metrics_spec.rb +35 -0
- data/spec/group_metric_spec.rb +26 -0
- data/spec/metric_data/base_spec.rb +0 -16
- data/spec/metric_data/uid_generator_spec.rb +40 -0
- data/spec/metric_spec.rb +23 -12
- data/spec/pirate_spec.rb +22 -1
- data/spec/redis_store_spec.rb +8 -2
- data/spec/storable_spec.rb +3 -0
- metadata +29 -3
- data/dashboard/views/metrics/_metric_data.erb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07e955101ffdd9c1bfe155e1916ebfc0a9911f4d
|
4
|
+
data.tar.gz: eaab8a527217bc44eaadf13665a230cd46e8ae65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d16393da029f3e584a1b632332582b20645dd1857b7b845012546bd0b939a99251969c6ec5e658fb425e61cb5cdf4ad2588461883a5128541f0a18ea66ca54d8
|
7
|
+
data.tar.gz: 5f5154181ac58db415657199eddb3f6e19ff236e999bf21bd74fc597903ab63350f783face5647bd534b5dd1b6c8580ffbe439616a5b812fc77320af3325e838
|
data/README.md
CHANGED
@@ -109,7 +109,7 @@ $pirate.context(user).add_metric(:referral)
|
|
109
109
|
|
110
110
|
If a context does not exist, `$pirate` will silently ignore all actions. This is useful for dealing with bots.
|
111
111
|
|
112
|
-
If the user is
|
112
|
+
If the user is unidentified set user to nil or false. If your app can return a Guest object for unidentified users, see the guest configuration setting.
|
113
113
|
|
114
114
|
### Collecting Metrics
|
115
115
|
|
@@ -215,33 +215,61 @@ if $pirate.context(user).feature_active?(:friend_feed){ ... }
|
|
215
215
|
|
216
216
|
### Defining groups
|
217
217
|
|
218
|
+
Group definitions are part of the configuration and should be defined
|
219
|
+
in an initializer.
|
220
|
+
|
218
221
|
```ruby
|
219
|
-
$pirate.
|
220
|
-
|
221
|
-
|
222
|
+
$pirate = Blackbeard.pirate do |config|
|
223
|
+
config.define_group(:admin) do |user, controller|
|
224
|
+
user && user.admin? # true, false
|
225
|
+
end
|
222
226
|
|
223
|
-
|
224
|
-
|
225
|
-
end
|
227
|
+
config.define_group(:seo_traffic) do |user, controller|
|
228
|
+
controller && controller.session.refer =~ /google.com/ # remember to store refer in sessions
|
229
|
+
end
|
226
230
|
|
227
|
-
|
228
|
-
|
229
|
-
end
|
231
|
+
config.define_group(:seo_traffic) do |user, controller|
|
232
|
+
controller && controller.session.refer =~ /google.com/ # remember to store refer in sessions
|
233
|
+
end
|
230
234
|
|
231
|
-
|
232
|
-
|
235
|
+
config.define_group(:purchasers) do |user, controller|
|
236
|
+
users && user.purchases.any?
|
237
|
+
end
|
233
238
|
end
|
234
239
|
```
|
240
|
+
The user will be nil for non-logged in visitors. The controller
|
241
|
+
will be nil if not defined by the context (e.g. outside a web context).
|
235
242
|
|
236
|
-
If your group is
|
243
|
+
If your group is segmented (doesn't return true or false), include a list possible segments.
|
237
244
|
|
238
245
|
```ruby
|
239
|
-
$pirate
|
240
|
-
|
246
|
+
$pirate = Blackbeard.pirate do |config|
|
247
|
+
...
|
248
|
+
config.define_group(:medalist, [:bronze, :silver, :gold]) do |user, context|
|
249
|
+
user.engagement_level # nil, :bronze, :silver, :gold
|
250
|
+
end
|
241
251
|
end
|
242
252
|
```
|
243
253
|
|
244
|
-
If your group definition block returns an uninitialized segment, it wil
|
254
|
+
If your group definition block returns an uninitialized segment, it wil
|
255
|
+
be initialized automatically.
|
256
|
+
|
257
|
+
### Defining Cohorts
|
258
|
+
|
259
|
+
Groups and cohorts in the dictionary sense are identical. For our purposes,
|
260
|
+
cohorts are participants that experienced something at the same time. A participant
|
261
|
+
can only be in one cohort at a time, but can switch cohorts.
|
262
|
+
|
263
|
+
This will add the current user/visitor and current timestamp to the cohort.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
$pirate.add_to_cohort(:joined_at) # returns false if already in cohort, otherwise true
|
267
|
+
$pirate.add_to_cohort!(:bought_at) # always returns true. If user is already in a cohort, the timestamp is updated.
|
268
|
+
$pirate.add_to_cohort(:joined_at, user.created_at) # You can optional pass in the timestamp
|
269
|
+
```
|
270
|
+
|
271
|
+
You'll can add cohorts to metrics compare how members who joined one day against
|
272
|
+
members who joined another day.
|
245
273
|
|
246
274
|
## Contributing
|
247
275
|
|
data/TODO.md
CHANGED
@@ -48,9 +48,11 @@ $pirate.funnel(:checkout, 'Confirm') # User reached step 3 of funnel (C
|
|
48
48
|
```
|
49
49
|
|
50
50
|
|
51
|
+
```ruby
|
51
52
|
$pirate.define_group(:achieve_pirate_style) do |user, context|
|
52
53
|
$pirate.metric(:pirate_style).achieved?
|
53
54
|
end
|
55
|
+
```
|
54
56
|
|
55
57
|
|
56
|
-
http://blog.sourcing.io/structuring-sinatra
|
58
|
+
[Structuring Sinatra applications](http://blog.sourcing.io/structuring-sinatra)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Blackbeard
|
2
|
+
module DashboardRoutes
|
3
|
+
class Cohorts < Base
|
4
|
+
get '/cohorts' do
|
5
|
+
@cohorts = Cohort.all
|
6
|
+
erb 'cohorts/index'.to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
get '/cohorts/:id' do
|
10
|
+
ensure_cohort
|
11
|
+
erb 'cohorts/show'.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
post "/cohorts/:id" do
|
15
|
+
ensure_cohort.update_attributes(params)
|
16
|
+
"OK"
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure_cohort
|
20
|
+
@cohort = Cohort.find(params[:id]) or pass
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -8,24 +8,27 @@ module Blackbeard
|
|
8
8
|
end
|
9
9
|
|
10
10
|
get "/features/:id" do
|
11
|
-
|
11
|
+
ensure_feature
|
12
12
|
@groups = Group.all
|
13
13
|
erb 'features/show'.to_sym
|
14
14
|
end
|
15
15
|
|
16
16
|
post "/features/:id" do
|
17
|
-
|
18
|
-
@feature.update_attributes(params)
|
17
|
+
ensure_feature.update_attributes(params)
|
19
18
|
"OK"
|
20
19
|
end
|
21
20
|
|
22
21
|
post "/features/:id/groups/:group_id" do
|
23
|
-
|
22
|
+
ensure_feature
|
24
23
|
@feature.set_segments_for(params[:group_id], params[:segments])
|
25
24
|
@feature.save
|
26
25
|
"OK"
|
27
26
|
end
|
28
27
|
|
28
|
+
def ensure_feature
|
29
|
+
@feature = Feature.find(params[:id]) or pass
|
30
|
+
end
|
31
|
+
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
data/dashboard/routes/groups.rb
CHANGED
@@ -7,16 +7,19 @@ module Blackbeard
|
|
7
7
|
end
|
8
8
|
|
9
9
|
get '/groups/:id' do
|
10
|
-
|
10
|
+
ensure_group
|
11
11
|
erb 'groups/show'.to_sym
|
12
12
|
end
|
13
13
|
|
14
14
|
post "/groups/:id" do
|
15
|
-
|
16
|
-
@group.update_attributes(params)
|
15
|
+
ensure_group.update_attributes(params)
|
17
16
|
"OK"
|
18
17
|
end
|
19
18
|
|
19
|
+
def ensure_group
|
20
|
+
@group = Group.find(params[:id]) or pass
|
21
|
+
end
|
22
|
+
|
20
23
|
end
|
21
24
|
end
|
22
25
|
end
|
data/dashboard/routes/metrics.rb
CHANGED
@@ -7,24 +7,44 @@ module Blackbeard
|
|
7
7
|
end
|
8
8
|
|
9
9
|
get "/metrics/:type/:type_id" do
|
10
|
-
|
11
|
-
@group = Group.find(params[:group_id]) if params[:group_id]
|
10
|
+
ensure_metric; find_group; find_cohort; ensure_charts
|
12
11
|
erb 'metrics/show'.to_sym
|
13
12
|
end
|
14
13
|
|
15
14
|
post "/metrics/:type/:type_id" do
|
16
|
-
|
15
|
+
ensure_metric
|
17
16
|
@metric.update_attributes(params)
|
18
17
|
"OK"
|
19
18
|
end
|
20
19
|
|
21
20
|
post "/metrics/:type/:type_id/groups" do
|
22
|
-
|
23
|
-
@group = Group.find(params[:group_id])
|
21
|
+
ensure_metric; find_group
|
24
22
|
@metric.add_group(@group) if @group
|
25
23
|
redirect url("metrics/#{@metric.type}/#{@metric.type_id}?group_id=#{@group.id}")
|
26
24
|
end
|
27
25
|
|
26
|
+
post "/metrics/:type/:type_id/cohorts" do
|
27
|
+
ensure_metric; find_cohort
|
28
|
+
@metric.add_cohort(@cohort) if @cohort
|
29
|
+
redirect url("metrics/#{@metric.type}/#{@metric.type_id}?cohort_id=#{@cohort.id}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_metric
|
33
|
+
@metric = Metric.find(params[:type], params[:type_id]) or pass
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_group
|
37
|
+
@group = Group.find(params[:group_id]) if params[:group_id]
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_cohort
|
41
|
+
@cohort = Cohort.find(params[:cohort_id]) if params[:cohort_id]
|
42
|
+
end
|
43
|
+
|
44
|
+
def ensure_charts
|
45
|
+
@charts = []
|
46
|
+
end
|
47
|
+
|
28
48
|
end
|
29
49
|
end
|
30
50
|
end
|
data/dashboard/routes/tests.rb
CHANGED
@@ -8,16 +8,19 @@ module Blackbeard
|
|
8
8
|
end
|
9
9
|
|
10
10
|
get "/tests/:id" do
|
11
|
-
|
11
|
+
ensure_test
|
12
12
|
erb 'tests/show'.to_sym
|
13
13
|
end
|
14
14
|
|
15
15
|
post "/tests/:id" do
|
16
|
-
|
17
|
-
@test.update_attributes(params)
|
16
|
+
ensure_test.update_attributes(params)
|
18
17
|
"OK"
|
19
18
|
end
|
20
19
|
|
20
|
+
def ensure_test
|
21
|
+
@test = Test.find(params[:id]) or pass
|
22
|
+
end
|
23
|
+
|
21
24
|
end
|
22
25
|
end
|
23
26
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1>Cohorts</h1>
|
3
|
+
<p>Are groupings of visitors that experience a certain event at the same time.</p>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<% if @cohorts.any? %>
|
7
|
+
<div class="panel panel-primary">
|
8
|
+
<div class="panel-heading">
|
9
|
+
<h3 class="panel-title">Active Cohorts</h3>
|
10
|
+
</div>
|
11
|
+
<ul class="list-group">
|
12
|
+
<% @cohorts.each do |cohort| %>
|
13
|
+
<li class="list-group-item"><a href="<%= url("cohorts/#{cohort.id}") %>"><%= cohort.name %></a></li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<% else %>
|
19
|
+
<div class="alert alert-danger">
|
20
|
+
<strong>No cohorts!</strong> Something may be wrong or you may just need to define some cohorts.
|
21
|
+
</div>
|
22
|
+
<% end %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1><span id="editable-name"><%= @cohort.name %></span> <small>Group</small></h1>
|
3
|
+
<p id="editable-description"><%= @cohort.description %></p>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<!--Load the AJAX API-->
|
7
|
+
<script src="<%= url('javascripts/jquery-1.10.2.min.js') %>"></script>
|
8
|
+
<script src="<%= url('javascripts/bootstrap.min.js') %>"></script>
|
9
|
+
<script src="<%= url('bootstrap3-editable/js/bootstrap-editable.min.js') %>"></script>
|
10
|
+
<script type="text/javascript">
|
11
|
+
//turn to inline mode
|
12
|
+
$.fn.editable.defaults.send = 'always';
|
13
|
+
$.fn.editable.defaults.params = function(params) {
|
14
|
+
var obj = {};
|
15
|
+
obj[params.name] = params.value;
|
16
|
+
return obj;
|
17
|
+
}
|
18
|
+
|
19
|
+
$(document).ready(function() {
|
20
|
+
$('#editable-name').editable({
|
21
|
+
placement: 'right',
|
22
|
+
type: 'text',
|
23
|
+
url: '<%= url("groups/#{@cohort.id}") %>',
|
24
|
+
name: 'name',
|
25
|
+
title: 'Edit cohort name',
|
26
|
+
validate: function(value) {
|
27
|
+
if($.trim(value) == '') {
|
28
|
+
return 'This field is required';
|
29
|
+
}
|
30
|
+
}
|
31
|
+
});
|
32
|
+
$('#editable-description').editable({
|
33
|
+
placement: 'bottom',
|
34
|
+
type: 'textarea',
|
35
|
+
url: '<%= url("metrics/#{@cohort.id}") %>',
|
36
|
+
name: 'description',
|
37
|
+
title: 'Edit cohort description'
|
38
|
+
});
|
39
|
+
});
|
40
|
+
</script>
|
41
|
+
<%= partial :'shared/charts', :locals => {:charts => [@cohort.recent_hours_chart, @cohort.recent_days_chart]} %>
|
@@ -50,9 +50,9 @@
|
|
50
50
|
$('#editable-description').editable({
|
51
51
|
placement: 'bottom',
|
52
52
|
type: 'textarea',
|
53
|
-
url: '<%= url("
|
53
|
+
url: '<%= url("groups/#{@group.id}") %>',
|
54
54
|
name: 'description',
|
55
|
-
title: 'Edit
|
55
|
+
title: 'Edit group description'
|
56
56
|
});
|
57
57
|
});
|
58
58
|
</script>
|
data/dashboard/views/layout.erb
CHANGED
@@ -33,6 +33,7 @@
|
|
33
33
|
</div>
|
34
34
|
<div class="navbar-collapse collapse">
|
35
35
|
<ul class="nav navbar-nav">
|
36
|
+
<li><a href="<%= url('cohorts') %>">Cohorts</a></li>
|
36
37
|
<li><a href="<%= url('features') %>">Features</a></li>
|
37
38
|
<li><a href="<%= url('groups') %>">Groups</a></li>
|
38
39
|
<li><a href="<%= url('metrics') %>">Metrics</a></li>
|
@@ -3,21 +3,38 @@
|
|
3
3
|
<p id="editable-description"><%= @metric.description %></p>
|
4
4
|
</div>
|
5
5
|
<form id="add_group" action="<%= url("/metrics/#{@metric.type}/#{@metric.type_id}/groups") %>" method="post"><input id="add_group_input" type="hidden" name="group_id"></form>
|
6
|
+
<form id="add_cohort" action="<%= url("/metrics/#{@metric.type}/#{@metric.type_id}/cohorts") %>" method="post"><input id="add_cohort_input" type="hidden" name="cohort_id"></form>
|
6
7
|
<ul class="nav nav-tabs">
|
7
|
-
<% if @group.nil? %><li class="active"><% else %><li><% end %><a href="?">All</a></li>
|
8
|
+
<% if @group.nil? && @cohort.nil? %><li class="active"><% else %><li><% end %><a href="?">All</a></li>
|
8
9
|
<% @metric.groups.each do |group| %>
|
9
10
|
<% if @group == group %><li class="active"><% else %><li><% end %><a href="?group_id=<%= group.id %>"><%= group.name %></a></li>
|
10
11
|
<% end %>
|
11
|
-
<%
|
12
|
+
<% @metric.cohorts.each do |cohort| %>
|
13
|
+
<% if @cohort == cohort %><li class="active"><% else %><li><% end %><a href="?cohort_id=<%= cohort.id %>"><%= cohort.name %></a></li>
|
14
|
+
<% end %>
|
15
|
+
|
12
16
|
<li class="dropdown">
|
13
|
-
<a id="drop5" role="button" data-toggle="dropdown" href="#">
|
17
|
+
<a id="drop5" role="button" data-toggle="dropdown" href="#">Add <b class="caret"></b></a>
|
14
18
|
<ul id="menu2" class="dropdown-menu" role="menu" aria-labelledby="drop5">
|
15
|
-
<% @metric.addable_groups.
|
16
|
-
|
19
|
+
<% if @metric.addable_groups.any? %>
|
20
|
+
<li role="presentation" class="dropdown-header">Group Metrics</li>
|
21
|
+
<% @metric.addable_groups.each do |group| %>
|
22
|
+
<li role="presentation" data-group-id="<%= group.id %>" data-group-name="<%= group.name %>" class="add-group"><a role="menuitem" tabindex="-1" href="#"><%= group.name %></a></li>
|
23
|
+
<% end %>
|
24
|
+
<% else %>
|
25
|
+
<li role="presentation" class="dropdown-header">No Group Metrics</li>
|
26
|
+
<% end %>
|
27
|
+
<li role="presentation" class="divider"></li>
|
28
|
+
<% if @metric.addable_cohorts.any? %>
|
29
|
+
<li role="presentation" class="dropdown-header">Cohort Metrics</li>
|
30
|
+
<% @metric.addable_cohorts.each do |cohort| %>
|
31
|
+
<li role="presentation" data-cohort-id="<%= cohort.id %>" data-cohort-name="<%= cohort.name %>" class="add-cohort"><a role="menuitem" tabindex="-1" href="#"><%= cohort.name %></a></li>
|
32
|
+
<% end %>
|
33
|
+
<% else %>
|
34
|
+
<li role="presentation" class="dropdown-header">No Cohort Metrics</li>
|
17
35
|
<% end %>
|
18
36
|
</ul>
|
19
37
|
</li>
|
20
|
-
<% end %>
|
21
38
|
</ul>
|
22
39
|
|
23
40
|
<!--Load the AJAX API-->
|
@@ -37,6 +54,19 @@
|
|
37
54
|
}
|
38
55
|
});
|
39
56
|
|
57
|
+
$('ul.nav-tabs').on("click", ".add-cohort", function (e) {
|
58
|
+
e.preventDefault();
|
59
|
+
var cohortId = $(this).data("cohort-id");
|
60
|
+
var cohortName = $(this).data("cohort-name");
|
61
|
+
|
62
|
+
if(confirm("Start collecting data by cohort "+cohortName+"?")) {
|
63
|
+
$('#add_cohort_input').val(cohortId);
|
64
|
+
$('#add_cohort').submit();
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
|
69
|
+
|
40
70
|
//turn to inline mode
|
41
71
|
$.fn.editable.defaults.send = 'always';
|
42
72
|
$.fn.editable.defaults.params = function(params) {
|
@@ -67,7 +97,4 @@
|
|
67
97
|
});
|
68
98
|
});
|
69
99
|
</script>
|
70
|
-
<%= partial :'
|
71
|
-
|
72
|
-
|
73
|
-
|
100
|
+
<%= partial :'shared/charts', :locals => {:charts => @charts } %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
2
|
+
<script type="text/javascript">
|
3
|
+
// Load the Visualization API and the piechart package.
|
4
|
+
google.load('visualization', '1.0', {'packages':['corechart']});
|
5
|
+
</script>
|
6
|
+
|
7
|
+
<% charts.each do |chart| %>
|
8
|
+
<div id="<%= chart.dom_id %>"></div>
|
9
|
+
<script type="text/javascript">
|
10
|
+
// Set a callback to run when the Google Visualization API is loaded.
|
11
|
+
google.setOnLoadCallback(
|
12
|
+
function(){
|
13
|
+
var data = new google.visualization.DataTable(jQuery.parseJSON('<%= chart.data.to_json %>'));
|
14
|
+
var options = jQuery.parseJSON('<%= chart.options.to_json %>');
|
15
|
+
var div = document.getElementById('<%= chart.dom_id %>');
|
16
|
+
new google.visualization.LineChart(div).draw(data, options);
|
17
|
+
});
|
18
|
+
|
19
|
+
</script>
|
20
|
+
<% end %>
|