ahoy_captain 0.77 → 0.81
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/app/assets/javascript/ahoy_captain/application.js +4 -4
- data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
- data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +9 -10
- data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
- data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
- data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +64 -58
- data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +11 -8
- data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +77 -133
- data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
- data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +10 -0
- data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +22 -22
- data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +10 -10
- data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +7 -8
- data/app/controllers/ahoy_captain/application_controller.rb +17 -15
- data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
- data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
- data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
- data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
- data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
- data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
- data/app/controllers/ahoy_captain/exports_controller.rb +15 -0
- data/app/controllers/ahoy_captain/realtimes_controller.rb +1 -1
- data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
- data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
- data/app/controllers/ahoy_captain/stats/base_controller.rb +61 -0
- data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +3 -2
- data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +1 -1
- data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +1 -1
- data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +1 -1
- data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +7 -6
- data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +1 -1
- data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
- data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
- data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
- data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
- data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
- data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
- data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
- data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
- data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
- data/app/models/ahoy_captain/export.rb +48 -0
- data/app/presenters/ahoy_captain/dashboard_presenter.rb +24 -16
- data/app/presenters/ahoy_captain/funnel_presenter.rb +32 -29
- data/app/presenters/ahoy_captain/goals_presenter.rb +32 -23
- data/app/queries/ahoy_captain/application_query.rb +5 -2
- data/app/queries/ahoy_captain/campaign_query.rb +14 -0
- data/app/queries/ahoy_captain/city_query.rb +11 -0
- data/app/queries/ahoy_captain/country_query.rb +10 -0
- data/app/queries/ahoy_captain/device_query.rb +10 -0
- data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
- data/app/queries/ahoy_captain/event_query.rb +16 -1
- data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
- data/app/queries/ahoy_captain/region_query.rb +11 -0
- data/app/queries/ahoy_captain/source_query.rb +10 -0
- data/app/queries/ahoy_captain/stats/average_visit_duration_query.rb +3 -7
- data/app/queries/ahoy_captain/stats/bounce_rates_query.rb +9 -6
- data/app/queries/ahoy_captain/stats/total_visitors_query.rb +1 -1
- data/app/queries/ahoy_captain/stats/unique_visitors_query.rb +1 -1
- data/app/queries/ahoy_captain/stats/views_per_visit_query.rb +2 -2
- data/app/queries/ahoy_captain/stats/visit_duration_query.rb +4 -4
- data/app/queries/ahoy_captain/top_page_query.rb +13 -0
- data/app/queries/ahoy_captain/visit_query.rb +12 -12
- data/app/views/ahoy_captain/goals/index.html.erb +5 -5
- data/app/views/ahoy_captain/roots/show.html.erb +1 -1
- data/app/views/ahoy_captain/stats/base/index.html.erb +9 -1
- data/app/views/ahoy_captain/stats/show.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/ahoy_captain/ahoy/event_methods.rb +1 -1
- data/lib/ahoy_captain/configuration.rb +2 -2
- data/lib/ahoy_captain/engine.rb +1 -0
- data/lib/ahoy_captain/goals.rb +8 -4
- data/lib/ahoy_captain/period_collection.rb +1 -1
- data/lib/ahoy_captain/version.rb +1 -1
- data/lib/generators/ahoy_captain/migration_generator.rb +21 -0
- data/lib/generators/ahoy_captain/templates/config.rb.tt +18 -3
- data/lib/generators/ahoy_captain/templates/migration.rb.tt +7 -0
- metadata +42 -4
- data/app/models/ahoy_captain/current.rb +0 -9
- data/app/models/ahoy_captain/url_helpers.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be1cf9a1c2b19db594abc06b05d15085bc180fe720986671f9016f1d660feec5
|
|
4
|
+
data.tar.gz: e447a70fc694ae39ad8500661a5726eb73ecd4777a0c6d9b4f4bc7d1fdee0541
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cef676f367fd05a6b080db63106ab748347b32316f93245a1b1f1bed2bca99a1cb6df185b168721629eae537d14167ef494fc42f6ad1cb24d9eabbd1160ae0f1
|
|
7
|
+
data.tar.gz: af29e7be6843d2ceb9922b5e234219c6466e72dda75d992078cf6a95c20e52fe62f6e870f6eaef68d546d00beaf4179222b4bd504eee2666274e571bc158f2d3
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import '@hotwired/turbo-rails';
|
|
2
|
+
import 'controllers';
|
|
3
|
+
import 'chartkick';
|
|
4
|
+
import 'Chart.bundle';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Application } from
|
|
1
|
+
import { Application } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
const application = Application.start()
|
|
3
|
+
const application = Application.start();
|
|
4
4
|
|
|
5
5
|
// Configure Stimulus development experience
|
|
6
|
-
application.debug = false
|
|
7
|
-
window.Stimulus = application
|
|
6
|
+
application.debug = false;
|
|
7
|
+
window.Stimulus = application;
|
|
8
8
|
|
|
9
|
-
export { application }
|
|
9
|
+
export { application };
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {Controller} from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
4
|
+
connect() {
|
|
5
|
+
if (new URLSearchParams(window.location.search).get('period') === 'realtime') {
|
|
6
|
+
this.element.querySelectorAll('turbo-frame').forEach((frame) => {
|
|
7
|
+
setInterval(() => {
|
|
8
|
+
frame.reload();
|
|
9
|
+
}, 1000 * 30);
|
|
10
|
+
});
|
|
12
11
|
}
|
|
13
|
-
|
|
12
|
+
}
|
|
14
13
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {Controller} from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static values = {
|
|
5
|
-
target: String
|
|
6
|
-
}
|
|
7
|
-
|
|
5
|
+
target: String,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
8
|
connect() {
|
|
9
9
|
this.modal = document.querySelector('#detailsModal');
|
|
10
10
|
this.turboFrame = document.querySelector('#detailsModal turbo-frame');
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
openModal(e) {
|
|
14
14
|
e.preventDefault();
|
|
15
15
|
this.modal.showModal();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {Controller} from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = [
|
|
4
|
+
static targets = ['label', 'close'];
|
|
5
5
|
|
|
6
6
|
setLabel(event) {
|
|
7
7
|
this.labelTarget.innerText = event.target.innerText;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {Controller} from
|
|
2
|
-
import SlimSelect from 'slim-select'
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import SlimSelect from 'slim-select';
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
5
|
static values = {
|
|
6
6
|
url: String,
|
|
7
7
|
column: String,
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
static targets = ['select', 'predicate'];
|
|
11
|
+
|
|
10
12
|
connect() {
|
|
11
13
|
this.selectTargets.forEach(async (target) => {
|
|
12
14
|
const url = target.dataset.filterUrlValue;
|
|
@@ -20,29 +22,30 @@ export default class extends Controller {
|
|
|
20
22
|
searchText: 'Sorry, no results found',
|
|
21
23
|
searchPlaceholder: 'Type to populate results',
|
|
22
24
|
placeholderText: `Search for ${target.dataset.filterColumnValue}`,
|
|
23
|
-
searchHighlight: true
|
|
25
|
+
searchHighlight: true,
|
|
24
26
|
},
|
|
25
27
|
events: {
|
|
26
28
|
beforeOpen: async () => {
|
|
27
|
-
if (!this.#hasData(target)
|
|
29
|
+
if (!this.#hasData(target) && !this.#hasSelections(target)) {
|
|
28
30
|
const data = await optionsSearch();
|
|
29
31
|
target.slim.setData(data);
|
|
30
32
|
}
|
|
31
33
|
},
|
|
32
34
|
search: async (search, currentData) => {
|
|
33
35
|
const data = await optionsSearch(search);
|
|
34
|
-
|
|
36
|
+
// eslint-disable-next-line max-len
|
|
37
|
+
const filteredData = data.filter((item) => !currentData.some((selectedItem) => selectedItem.text === item.text));
|
|
35
38
|
return filteredData;
|
|
36
39
|
},
|
|
37
40
|
beforeChange: this.#beforeChange(target, url),
|
|
38
|
-
}
|
|
41
|
+
},
|
|
39
42
|
});
|
|
40
|
-
const json = JSON.parse(target.dataset.filterSelected)
|
|
41
|
-
if(json.length) {
|
|
42
|
-
select.setData(json.map(item => ({
|
|
43
|
-
select.setSelected(json)
|
|
43
|
+
const json = JSON.parse(target.dataset.filterSelected);
|
|
44
|
+
if (json.length) {
|
|
45
|
+
select.setData(json.map((item) => ({ text: item, value: item })));
|
|
46
|
+
select.setSelected(json);
|
|
44
47
|
}
|
|
45
|
-
})
|
|
48
|
+
});
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
fetchOptions(url, target) {
|
|
@@ -51,42 +54,41 @@ export default class extends Controller {
|
|
|
51
54
|
const response = await fetch(`${url}?${query.toString()}`);
|
|
52
55
|
const data = await response.json();
|
|
53
56
|
return data;
|
|
54
|
-
}
|
|
57
|
+
};
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
#buildQueryFilters(search, target) {
|
|
58
61
|
const query = new URLSearchParams(window.location.search);
|
|
59
|
-
const otherFilters = this.selectTargets.filter(filter => filter
|
|
60
|
-
otherFilters.forEach(
|
|
61
|
-
filter.slim.getSelected().forEach(val => {
|
|
62
|
-
const filterName = `${filter.name}[]
|
|
63
|
-
if(query.has(filterName)) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
query.append(filterName, value)
|
|
62
|
+
const otherFilters = this.selectTargets.filter((filter) => filter !== target);
|
|
63
|
+
otherFilters.forEach((filter) => {
|
|
64
|
+
filter.slim.getSelected().forEach((val) => {
|
|
65
|
+
const filterName = `${filter.name}[]`;
|
|
66
|
+
if (query.has(filterName)) {
|
|
67
|
+
if (!query.get(`${filter.name}[]`).includes(val)) {
|
|
68
|
+
query.append(filterName, val);
|
|
67
69
|
}
|
|
68
70
|
} else {
|
|
69
|
-
query.append(filterName,
|
|
71
|
+
query.append(filterName, val);
|
|
70
72
|
}
|
|
71
|
-
})
|
|
73
|
+
});
|
|
72
74
|
});
|
|
73
|
-
query.set(`q[${target.dataset.filterColumnValue}_i_cont]`, search ||
|
|
75
|
+
query.set(`q[${target.dataset.filterColumnValue}_i_cont]`, search || '');
|
|
74
76
|
return query;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
#beforeChange(target
|
|
79
|
+
#beforeChange(target) {
|
|
78
80
|
return () => {
|
|
79
|
-
const otherFilters = this.selectTargets.filter(filter => filter
|
|
80
|
-
otherFilters.forEach(async
|
|
81
|
-
if (this.#hasData(
|
|
82
|
-
const selected =
|
|
83
|
-
|
|
81
|
+
const otherFilters = this.selectTargets.filter((filter) => filter !== target);
|
|
82
|
+
otherFilters.forEach(async (filter) => {
|
|
83
|
+
if (this.#hasData(filter)) {
|
|
84
|
+
const selected = filter.slim.getSelected();
|
|
85
|
+
filter.slim.setData(selected.map((text) => ({ text })));
|
|
84
86
|
// setting data triggers a slim render, so need to also set selected again
|
|
85
|
-
|
|
87
|
+
filter.slim.setSelected(selected);
|
|
86
88
|
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
}
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
#hasData(target) {
|
|
@@ -98,48 +100,52 @@ export default class extends Controller {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
#filtersForQuery() {
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// eslint-disable-next-line max-len
|
|
104
|
+
const predicates = this.predicateTargets.map((el) => ({ name: el.name, column_predicate: el.value }));
|
|
105
|
+
// eslint-disable-next-line max-len
|
|
106
|
+
const filterValues = this.selectTargets.map((el) => ({ name: el.name, selections: el.slim.getSelected() }));
|
|
107
|
+
const mergedData = predicates.map((predicate) => {
|
|
108
|
+
const matchingFilter = filterValues.find((filter) => filter.name === predicate.name);
|
|
105
109
|
if (matchingFilter.selections.length > 0) {
|
|
106
|
-
return {...predicate, ...matchingFilter}
|
|
110
|
+
return { ...predicate, ...matchingFilter };
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
return undefined;
|
|
113
|
+
}).filter((el) => el !== undefined);
|
|
109
114
|
return mergedData;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
resetFilters(event) {
|
|
113
|
-
this.selectTargets.forEach(el => {
|
|
114
|
-
el.slim.setSelected([])
|
|
115
|
-
})
|
|
116
|
-
this.applyFilters(event)
|
|
118
|
+
this.selectTargets.forEach((el) => {
|
|
119
|
+
el.slim.setSelected([]);
|
|
120
|
+
});
|
|
121
|
+
this.applyFilters(event);
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
applyFilters(e) {
|
|
120
125
|
e.preventDefault();
|
|
121
126
|
const searchParams = new URLSearchParams(window.location.search);
|
|
122
127
|
const filters = this.#filtersForQuery();
|
|
123
|
-
filters.forEach(filter => {
|
|
124
|
-
const name = `q[${filter.column_predicate}][]
|
|
125
|
-
const selectedValues = searchParams.getAll(name)
|
|
126
|
-
filter.selections.forEach(selection => {
|
|
127
|
-
if(selectedValues) {
|
|
128
|
-
if(!selectedValues.includes(selection)) {
|
|
129
|
-
searchParams.append(name, selection)
|
|
128
|
+
filters.forEach((filter) => {
|
|
129
|
+
const name = `q[${filter.column_predicate}][]`;
|
|
130
|
+
const selectedValues = searchParams.getAll(name);
|
|
131
|
+
filter.selections.forEach((selection) => {
|
|
132
|
+
if (selectedValues) {
|
|
133
|
+
if (!selectedValues.includes(selection)) {
|
|
134
|
+
searchParams.append(name, selection);
|
|
130
135
|
}
|
|
131
136
|
} else {
|
|
132
|
-
searchParams.append(name, selection)
|
|
137
|
+
searchParams.append(name, selection);
|
|
133
138
|
}
|
|
134
|
-
})
|
|
139
|
+
});
|
|
135
140
|
});
|
|
136
|
-
['input[name="start_date"]', 'input[name="end_date"]'].forEach(selector => {
|
|
137
|
-
const el = document.querySelector(selector)
|
|
138
|
-
if(el.value.length) {
|
|
139
|
-
searchParams.delete(
|
|
141
|
+
['input[name="start_date"]', 'input[name="end_date"]'].forEach((selector) => {
|
|
142
|
+
const el = document.querySelector(selector);
|
|
143
|
+
if (el.value.length) {
|
|
144
|
+
searchParams.delete('period');
|
|
140
145
|
}
|
|
141
146
|
searchParams.set(el.name, el.value);
|
|
142
147
|
});
|
|
143
|
-
|
|
148
|
+
|
|
149
|
+
Turbo.visit(`${window.location.pathname.replace(/\/$/, '')}?${searchParams.toString()}`);
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {Controller} from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = [
|
|
4
|
+
static targets = ['delete'];
|
|
5
|
+
|
|
5
6
|
static values = {
|
|
6
7
|
columnPredicate: String,
|
|
7
|
-
category: String
|
|
8
|
-
}
|
|
8
|
+
category: String,
|
|
9
|
+
};
|
|
9
10
|
|
|
10
11
|
remove(event) {
|
|
11
12
|
event.preventDefault();
|
|
12
|
-
this.dispatch('remove', {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
this.dispatch('remove', {
|
|
14
|
+
detail: {
|
|
15
|
+
paramKey: `q[${this.columnPredicateValue}]`,
|
|
16
|
+
paramValue: this.categoryValue,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
16
19
|
}
|
|
17
20
|
}
|
|
@@ -1,143 +1,87 @@
|
|
|
1
|
-
import {Controller} from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
import 'chartjs-plugin-datalabels';
|
|
3
|
-
const THOUSAND = 1000
|
|
4
|
-
const HUNDRED_THOUSAND = 100000
|
|
5
|
-
const MILLION = 1000000
|
|
6
|
-
const HUNDRED_MILLION = 100000000
|
|
7
|
-
const BILLION = 1000000000
|
|
8
|
-
const HUNDRED_BILLION = 100000000000
|
|
9
|
-
const TRILLION = 1000000000000
|
|
10
|
-
|
|
11
|
-
function numberFormatter(num) {
|
|
12
|
-
if (num >= THOUSAND && num < MILLION) {
|
|
13
|
-
const thousands = num / THOUSAND
|
|
14
|
-
if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
|
|
15
|
-
return Math.floor(thousands) + 'k'
|
|
16
|
-
} else {
|
|
17
|
-
return (Math.floor(thousands * 10) / 10) + 'k'
|
|
18
|
-
}
|
|
19
|
-
} else if (num >= MILLION && num < BILLION) {
|
|
20
|
-
const millions = num / MILLION
|
|
21
|
-
if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
|
|
22
|
-
return Math.floor(millions) + 'M'
|
|
23
|
-
} else {
|
|
24
|
-
return (Math.floor(millions * 10) / 10) + 'M'
|
|
25
|
-
}
|
|
26
|
-
} else if (num >= BILLION && num < TRILLION) {
|
|
27
|
-
const billions = num / BILLION
|
|
28
|
-
if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
|
|
29
|
-
return Math.floor(billions) + 'B'
|
|
30
|
-
} else {
|
|
31
|
-
return (Math.floor(billions * 10) / 10) + 'B'
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
return num
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
3
|
|
|
38
4
|
export default class extends Controller {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const getPalette = () => {
|
|
43
|
-
return {
|
|
44
|
-
dataLabelBackground: 'rgba(25, 30, 56, 0.97)',
|
|
45
|
-
dataLabelTextColor: 'rgb(243, 244, 246)',
|
|
46
|
-
visitorsBackground: 'rgb(99, 102, 241)',
|
|
47
|
-
dropoffBackground: '#2F3949',
|
|
48
|
-
dropoffStripes: 'rgb(25, 30, 56)',
|
|
49
|
-
stepNameLegendColor: 'rgb(228, 228, 231)',
|
|
50
|
-
visitorsLegendClass: 'bg-indigo-500',
|
|
51
|
-
dropoffLegendClass: 'bg-gray-600',
|
|
52
|
-
smallBarClass: 'bg-indigo-500'
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
5
|
+
connect() {
|
|
6
|
+
const funnel = JSON.parse(this.element.dataset.data);
|
|
56
7
|
|
|
8
|
+
const fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
|
|
9
|
+
const labels = funnel.steps.map((step) => step.name);
|
|
10
|
+
const stepData = funnel.steps.map((step) => step.total_events);
|
|
11
|
+
const dropOffData = funnel.steps.map((step) => step.drop_off * 100);
|
|
57
12
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
13
|
+
const data = {
|
|
14
|
+
labels,
|
|
15
|
+
datasets: [
|
|
16
|
+
{
|
|
17
|
+
label: 'Visitors',
|
|
18
|
+
data: stepData,
|
|
19
|
+
borderRadius: 4,
|
|
20
|
+
stack: 'Stack 0',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: 'Dropoff',
|
|
24
|
+
data: dropOffData,
|
|
25
|
+
borderRadius: 4,
|
|
26
|
+
stack: 'Stack 0',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
66
30
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
datalabels: {
|
|
111
|
-
anchor: 'end',
|
|
112
|
-
align: 'end',
|
|
113
|
-
borderRadius: 4,
|
|
114
|
-
padding: { top: 8, bottom: 8, right: 8, left: 8 },
|
|
115
|
-
font: { size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily },
|
|
116
|
-
textAlign: 'center',
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
scales: {
|
|
120
|
-
y: { display: false },
|
|
121
|
-
x: {
|
|
122
|
-
position: 'bottom',
|
|
123
|
-
display: true,
|
|
124
|
-
border: { display: false },
|
|
125
|
-
grid: { drawBorder: false, display: false },
|
|
126
|
-
ticks: {
|
|
127
|
-
padding: 8,
|
|
128
|
-
font: { weight: 'bold', family: fontFamily, size: 14 },
|
|
129
|
-
color: 'rgb(228, 228, 231)'
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
},
|
|
31
|
+
const config = {
|
|
32
|
+
responsive: true,
|
|
33
|
+
maintainAspectRatio: false,
|
|
34
|
+
plugins: [ChartDataLabels],
|
|
35
|
+
type: 'bar',
|
|
36
|
+
data,
|
|
37
|
+
options: {
|
|
38
|
+
layout: {
|
|
39
|
+
padding: 100,
|
|
40
|
+
},
|
|
41
|
+
plugins: {
|
|
42
|
+
legend: {
|
|
43
|
+
display: false,
|
|
44
|
+
},
|
|
45
|
+
tooltip: {
|
|
46
|
+
mode: 'index',
|
|
47
|
+
intersect: true,
|
|
48
|
+
position: 'average',
|
|
49
|
+
},
|
|
50
|
+
datalabels: {
|
|
51
|
+
anchor: 'end',
|
|
52
|
+
align: 'end',
|
|
53
|
+
borderRadius: 4,
|
|
54
|
+
padding: {
|
|
55
|
+
top: 8, bottom: 8, right: 8, left: 8,
|
|
56
|
+
},
|
|
57
|
+
font: {
|
|
58
|
+
size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily,
|
|
59
|
+
},
|
|
60
|
+
textAlign: 'center',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
scales: {
|
|
64
|
+
y: { display: false },
|
|
65
|
+
x: {
|
|
66
|
+
position: 'bottom',
|
|
67
|
+
display: true,
|
|
68
|
+
border: { display: false },
|
|
69
|
+
grid: { drawBorder: false, display: false },
|
|
70
|
+
ticks: {
|
|
71
|
+
padding: 8,
|
|
72
|
+
font: { weight: 'bold', family: fontFamily, size: 14 },
|
|
73
|
+
color: 'rgb(228, 228, 231)',
|
|
133
74
|
},
|
|
134
|
-
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
135
79
|
|
|
136
|
-
|
|
80
|
+
const visitorsData = [];
|
|
137
81
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
82
|
+
new Chart(
|
|
83
|
+
this.element,
|
|
84
|
+
config,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
143
87
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {application} from
|
|
2
|
-
import {eagerLoadControllersFrom} from
|
|
3
|
-
|
|
1
|
+
import { application } from 'controllers/application';
|
|
2
|
+
import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading';
|
|
3
|
+
|
|
4
|
+
eagerLoadControllersFrom('controllers', application);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
handleChange(event) {
|
|
5
|
+
const url = new URL(event.target.form.action);
|
|
6
|
+
const interval = event.target.value;
|
|
7
|
+
url.searchParams.set('interval', interval);
|
|
8
|
+
event.target.closest('turbo-frame').src = url.href;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { Controller } from
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = [
|
|
5
|
-
|
|
4
|
+
static targets = ['countriesLink', 'top_pagesLink', 'devicesLink', 'top_sourcesLink'];
|
|
5
|
+
|
|
6
|
+
static classes = ['countries', 'top_pages', 'devices', 'top_sources'];
|
|
6
7
|
|
|
7
8
|
changeCountries(event) {
|
|
8
|
-
|
|
9
|
-
event.currentTarget.classList.add(...this.countriesClasses)
|
|
9
|
+
event.currentTarget.classList.add(...this.countriesClasses);
|
|
10
10
|
this.countriesLinkTargets.forEach((target) => {
|
|
11
|
-
if (target
|
|
12
|
-
target.classList.remove(...this.countriesClasses)
|
|
11
|
+
if (target !== event.currentTarget) {
|
|
12
|
+
target.classList.remove(...this.countriesClasses);
|
|
13
13
|
}
|
|
14
|
-
})
|
|
14
|
+
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
changeTopSources(event) {
|
|
18
|
-
event.currentTarget.classList.add(...this.top_sourcesClasses)
|
|
18
|
+
event.currentTarget.classList.add(...this.top_sourcesClasses);
|
|
19
19
|
this.top_sourcesLinkTargets.forEach((target) => {
|
|
20
|
-
if (target
|
|
21
|
-
target.classList.remove(...this.top_sourcesClasses)
|
|
20
|
+
if (target !== event.currentTarget) {
|
|
21
|
+
target.classList.remove(...this.top_sourcesClasses);
|
|
22
22
|
}
|
|
23
|
-
})
|
|
23
|
+
});
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
changeTopPages(event) {
|
|
27
|
-
event.currentTarget.classList.add(...this.top_pagesClasses)
|
|
27
|
+
event.currentTarget.classList.add(...this.top_pagesClasses);
|
|
28
28
|
this.top_pagesLinkTargets.forEach((target) => {
|
|
29
|
-
if (target
|
|
30
|
-
target.classList.remove(...this.top_pagesClasses)
|
|
29
|
+
if (target !== event.currentTarget) {
|
|
30
|
+
target.classList.remove(...this.top_pagesClasses);
|
|
31
31
|
}
|
|
32
|
-
})
|
|
32
|
+
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
changeDevices(event) {
|
|
36
|
-
event.currentTarget.classList.add(...this.devicesClasses)
|
|
36
|
+
event.currentTarget.classList.add(...this.devicesClasses);
|
|
37
37
|
this.devicesLinkTargets.forEach((target) => {
|
|
38
|
-
if (target
|
|
39
|
-
target.classList.remove(...this.devicesClasses)
|
|
38
|
+
if (target !== event.currentTarget) {
|
|
39
|
+
target.classList.remove(...this.devicesClasses);
|
|
40
40
|
}
|
|
41
|
-
})
|
|
41
|
+
});
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|