ahoy_captain 0.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -13
  3. data/app/assets/javascript/ahoy_captain/application.js +4 -4
  4. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +30 -0
  5. data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
  6. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +16 -9
  7. data/app/assets/javascript/ahoy_captain/controllers/combobox_controller.js +341 -0
  8. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
  9. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
  10. data/app/assets/javascript/ahoy_captain/controllers/filter/item_controller.js +12 -0
  11. data/app/assets/javascript/ahoy_captain/controllers/filter_form_controller.js +13 -0
  12. data/app/assets/javascript/ahoy_captain/controllers/filter_modal_controller.js +45 -0
  13. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +116 -104
  14. data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
  15. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +8 -3
  16. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +188 -0
  17. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +9 -0
  18. data/app/assets/javascript/ahoy_captain/controllers/properties_controller.js +8 -0
  19. data/app/assets/javascript/ahoy_captain/controllers/property_filter_controller.js +46 -0
  20. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +12 -9
  21. data/app/assets/javascript/ahoy_captain/controllers/tile_controller.js +9 -0
  22. data/app/assets/javascript/ahoy_captain/controllers/toggle_controller.js +17 -0
  23. data/app/assets/javascript/ahoy_captain/helpers/chart_utils.js +156 -0
  24. data/app/assets/javascript/ahoy_captain/helpers/number_formatters.js +55 -0
  25. data/app/components/ahoy_captain/combobox_component.html.erb +33 -0
  26. data/app/components/ahoy_captain/combobox_component.rb +13 -0
  27. data/app/components/ahoy_captain/comparison_link_component.rb +40 -0
  28. data/app/components/ahoy_captain/dropdown_button_component.html.erb +5 -5
  29. data/app/components/ahoy_captain/dropdown_link_component.html.erb +5 -5
  30. data/app/components/ahoy_captain/filter/dropdown_component.html.erb +50 -0
  31. data/app/components/ahoy_captain/filter/dropdown_component.rb +51 -0
  32. data/app/components/ahoy_captain/filter/modal_component.html.erb +12 -9
  33. data/app/components/ahoy_captain/filter/select_component.html.erb +23 -19
  34. data/app/components/ahoy_captain/filter/select_component.rb +41 -9
  35. data/app/components/ahoy_captain/filter/tag_component.html.erb +8 -4
  36. data/app/components/ahoy_captain/filter/tag_component.rb +6 -30
  37. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +2 -3
  38. data/app/components/ahoy_captain/filter/tag_container_component.rb +1 -8
  39. data/app/components/ahoy_captain/stats/comparable_container_component.html.erb +25 -0
  40. data/app/components/ahoy_captain/stats/comparable_container_component.rb +86 -0
  41. data/app/components/ahoy_captain/stats/container_component.html.erb +15 -0
  42. data/app/components/ahoy_captain/stats/container_component.rb +26 -0
  43. data/app/components/ahoy_captain/sticky_nav_component.html.erb +13 -22
  44. data/app/components/ahoy_captain/sticky_nav_component.rb +11 -0
  45. data/app/components/ahoy_captain/table_component.html.erb +4 -37
  46. data/app/components/ahoy_captain/table_component.rb +25 -5
  47. data/app/components/ahoy_captain/tables/devices_table_component.rb +11 -0
  48. data/app/components/ahoy_captain/tables/dynamic_table.rb +13 -0
  49. data/app/components/ahoy_captain/tables/dynamic_table_component.rb +204 -0
  50. data/app/components/ahoy_captain/tables/goals_table_component.rb +17 -0
  51. data/app/components/ahoy_captain/tables/header_component.html.erb +5 -0
  52. data/app/components/ahoy_captain/tables/header_component.rb +18 -0
  53. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +5 -0
  54. data/app/components/ahoy_captain/tables/headers/header_component.rb +16 -0
  55. data/app/components/ahoy_captain/tables/properties_table_component.rb +27 -0
  56. data/app/components/ahoy_captain/tables/row_component.html.erb +4 -0
  57. data/app/components/ahoy_captain/tables/rows/row_component.html.erb +6 -0
  58. data/app/components/ahoy_captain/tables/rows/row_component.rb +40 -0
  59. data/app/components/ahoy_captain/tile_component.html.erb +21 -10
  60. data/app/components/ahoy_captain/tile_component.rb +3 -2
  61. data/app/components/ahoy_captain/tooltip_component.html.erb +2 -2
  62. data/app/controllers/ahoy_captain/application_controller.rb +19 -29
  63. data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
  64. data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
  65. data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
  66. data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
  67. data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
  68. data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
  69. data/app/controllers/ahoy_captain/exports_controller.rb +14 -0
  70. data/app/controllers/ahoy_captain/filters/base_controller.rb +1 -3
  71. data/app/controllers/ahoy_captain/filters/goals_controller.rb +9 -0
  72. data/app/controllers/ahoy_captain/filters/pages/actions_controller.rb +1 -1
  73. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +3 -3
  74. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +2 -3
  75. data/app/controllers/ahoy_captain/filters/properties/names_controller.rb +11 -0
  76. data/app/controllers/ahoy_captain/filters/properties/values_controller.rb +15 -0
  77. data/app/controllers/ahoy_captain/filters/sources_controller.rb +1 -1
  78. data/app/controllers/ahoy_captain/filters/utms_controller.rb +1 -1
  79. data/app/controllers/ahoy_captain/properties_controller.rb +41 -0
  80. data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
  81. data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
  82. data/app/controllers/ahoy_captain/stats/base_controller.rb +86 -5
  83. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +2 -1
  84. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +2 -1
  85. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +2 -1
  86. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +3 -1
  87. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +3 -11
  88. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +2 -1
  89. data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
  90. data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
  91. data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
  92. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  93. data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
  94. data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
  95. data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
  96. data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
  97. data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
  98. data/app/helpers/ahoy_captain/application_helper.rb +62 -3
  99. data/app/models/ahoy_captain/comparison_mode.rb +72 -0
  100. data/app/models/ahoy_captain/export.rb +48 -0
  101. data/app/models/ahoy_captain/filter_parser.rb +82 -0
  102. data/app/models/ahoy_captain/range_from_params.rb +75 -0
  103. data/app/models/ahoy_captain/rangeable.rb +0 -3
  104. data/app/models/concerns/ahoy_captain/compare_mode.rb +19 -0
  105. data/app/models/concerns/ahoy_captain/limitable.rb +17 -0
  106. data/app/models/concerns/ahoy_captain/range_options.rb +1 -14
  107. data/app/presenters/ahoy_captain/dashboard_presenter.rb +18 -49
  108. data/app/presenters/ahoy_captain/goals_presenter.rb +3 -2
  109. data/app/queries/ahoy_captain/application_query.rb +78 -13
  110. data/app/queries/ahoy_captain/campaign_query.rb +14 -0
  111. data/app/queries/ahoy_captain/city_query.rb +11 -0
  112. data/app/queries/ahoy_captain/country_query.rb +10 -0
  113. data/app/queries/ahoy_captain/device_query.rb +10 -0
  114. data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
  115. data/app/queries/ahoy_captain/event_query.rb +20 -13
  116. data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
  117. data/app/queries/ahoy_captain/region_query.rb +11 -0
  118. data/app/queries/ahoy_captain/source_query.rb +10 -0
  119. data/app/queries/ahoy_captain/stats/average_views_per_visit_query.rb +11 -4
  120. data/app/queries/ahoy_captain/stats/average_visit_duration_query.rb +14 -2
  121. data/app/queries/ahoy_captain/stats/base_query.rb +18 -0
  122. data/app/queries/ahoy_captain/stats/bounce_rates_query.rb +15 -1
  123. data/app/queries/ahoy_captain/stats/total_pageviews_query.rb +2 -2
  124. data/app/queries/ahoy_captain/stats/total_visitors_query.rb +1 -1
  125. data/app/queries/ahoy_captain/stats/unique_visitors_query.rb +1 -1
  126. data/app/queries/ahoy_captain/stats/views_per_visit_query.rb +1 -1
  127. data/app/queries/ahoy_captain/stats/visit_duration_query.rb +3 -3
  128. data/app/queries/ahoy_captain/top_page_query.rb +13 -0
  129. data/app/queries/ahoy_captain/visit_query.rb +2 -3
  130. data/app/queries/concerns/ahoy_captain/comparable_queries.rb +25 -0
  131. data/app/queries/concerns/ahoy_captain/comparable_query.rb +138 -0
  132. data/app/queries/concerns/ahoy_captain/lazy_comparable_query.rb +42 -0
  133. data/app/views/ahoy_captain/devices/_table.html.erb +2 -0
  134. data/app/views/ahoy_captain/devices/index.html+details.erb +1 -1
  135. data/app/views/ahoy_captain/devices/index.html.erb +2 -2
  136. data/app/views/ahoy_captain/funnels/show.html.erb +5 -2
  137. data/app/views/ahoy_captain/goals/index.html.erb +2 -37
  138. data/app/views/ahoy_captain/layouts/application.html.erb +3 -4
  139. data/app/views/ahoy_captain/properties/_form.html.erb +6 -0
  140. data/app/views/ahoy_captain/properties/index.html.erb +3 -0
  141. data/app/views/ahoy_captain/properties/show.html.erb +6 -0
  142. data/app/views/ahoy_captain/realtimes/show.html.erb +1 -1
  143. data/app/views/ahoy_captain/roots/_filters.html.erb +80 -0
  144. data/app/views/ahoy_captain/roots/show.html.erb +76 -109
  145. data/app/views/ahoy_captain/stats/base/index.html.erb +34 -9
  146. data/app/views/ahoy_captain/stats/show.html.erb +8 -55
  147. data/config/routes.rb +9 -0
  148. data/lib/ahoy_captain/ahoy/event_methods.rb +35 -74
  149. data/lib/ahoy_captain/ahoy/visit_methods.rb +1 -1
  150. data/lib/ahoy_captain/configuration.rb +18 -7
  151. data/lib/ahoy_captain/engine.rb +22 -0
  152. data/lib/ahoy_captain/filter_configuration/filter.rb +16 -0
  153. data/lib/ahoy_captain/filter_configuration/filter_collection.rb +48 -0
  154. data/lib/ahoy_captain/filters_configuration.rb +77 -0
  155. data/lib/ahoy_captain/goals.rb +10 -2
  156. data/lib/ahoy_captain/period_collection.rb +1 -1
  157. data/lib/ahoy_captain/predicate_label.rb +7 -0
  158. data/lib/ahoy_captain/version.rb +1 -1
  159. data/lib/ahoy_captain.rb +7 -1
  160. data/lib/generators/ahoy_captain/templates/config.rb.tt +32 -0
  161. metadata +80 -21
  162. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +0 -145
  163. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +0 -17
  164. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +0 -43
  165. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +0 -25
  166. data/app/models/ahoy_captain/current.rb +0 -9
  167. data/app/models/ahoy_captain/url_helpers.rb +0 -6
@@ -1,117 +1,129 @@
1
- import {Controller} from "@hotwired/stimulus"
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
3
+ import { getCSS, externalTooltipHandler } from "helpers/chart_utils";
10
4
 
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
- }
5
+ const calculatePercentageDifference = function(oldValue, newValue) {
6
+ if(!oldValue) { return false }
7
+ if (oldValue == 0 && newValue > 0) {
8
+ return 100
9
+ } else if (oldValue == 0 && newValue == 0) {
10
+ return 0
11
+ } else {
12
+ return Math.round((newValue - oldValue) / oldValue * 100)
13
+ }
36
14
  }
37
15
 
38
16
  export default class extends Controller {
39
- connect() {
40
- const funnel = JSON.parse(this.element.dataset.data);
17
+ connect() {
18
+ this.funnel = JSON.parse(this.element.dataset.data);
41
19
 
42
- 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"'
43
- const labels = funnel.steps.map((step) => step.name)
44
- const stepData = funnel.steps.map((step) => step.total_events)
45
- const dropOffData = funnel.steps.map((step) => step.drop_off * 100)
20
+ 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"';
21
+ const labels = this.funnel.steps.map((step) => step.name);
22
+ const stepData = this.funnel.steps.map((step) => step.total_events);
23
+ const dropOffData = this.funnel.steps.map((step) => step.drop_off * 100);
46
24
 
47
- const data = {
48
- labels: labels,
49
- datasets: [
50
- {
51
- label: 'Visitors',
52
- data: stepData,
53
- borderRadius: 4,
54
- stack: 'Stack 0',
55
- },
56
- {
57
- label: 'Dropoff',
58
- data: dropOffData,
59
- borderRadius: 4,
60
- stack: 'Stack 0',
61
- },
62
- ],
63
- }
25
+ const data = {
26
+ labels,
27
+ datasets: [
28
+ {
29
+ label: 'Visitors',
30
+ data: stepData,
31
+ borderRadius: 4,
32
+ color: getCSS('--ac'),
33
+ backgroundColor: getCSS('--p'),
34
+ stack: 'Stack 0',
35
+ yAxisID: 'y',
36
+ },
37
+ {
38
+ label: 'Dropoff',
39
+ data: dropOffData,
40
+ borderRadius: 4,
41
+ stack: 'Stack 0',
42
+ color: getCSS('--ac'),
43
+ backgroundColor: getCSS('--a'),
44
+ yAxisID: 'yComparison',
45
+ },
46
+ ],
47
+ };
64
48
 
65
- const config = {
66
- responsive:true,
67
- maintainAspectRatio: false,
68
- plugins: [ChartDataLabels],
69
- type: 'bar',
70
- data: data,
71
- options: {
72
- layout: {
73
- padding: 100,
74
- },
75
- plugins: {
76
- legend: {
77
- display: false,
78
- },
79
- tooltip: {
80
- mode: 'index',
81
- intersect: true,
82
- position: 'average',
83
- },
84
- datalabels: {
85
- anchor: 'end',
86
- align: 'end',
87
- borderRadius: 4,
88
- padding: { top: 8, bottom: 8, right: 8, left: 8 },
89
- font: { size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily },
90
- textAlign: 'center',
91
- },
92
- },
93
- scales: {
94
- y: { display: false },
95
- x: {
96
- position: 'bottom',
97
- display: true,
98
- border: { display: false },
99
- grid: { drawBorder: false, display: false },
100
- ticks: {
101
- padding: 8,
102
- font: { weight: 'bold', family: fontFamily, size: 14 },
103
- color: 'rgb(228, 228, 231)'
104
- },
105
- },
106
- },
49
+ const config = {
50
+ responsive: true,
51
+ maintainAspectRatio: false,
52
+ plugins: [ChartDataLabels],
53
+ type: 'bar',
54
+ data,
55
+ options: {
56
+ layout: {
57
+ padding: 100,
58
+ },
59
+ plugins: {
60
+ legend: false,
61
+ tooltip: {
62
+ enabled: false,
63
+ position: 'nearest',
64
+ external: externalTooltipHandler(this)
65
+ },
66
+ datalabels: {
67
+ anchor: 'end',
68
+ align: 'end',
69
+ borderRadius: 4,
70
+ padding: {
71
+ top: 8, bottom: 8, right: 8, left: 8,
72
+ },
73
+ color: getCSS('--pc'),
74
+ textAlign: 'center',
75
+ },
76
+ },
77
+ scales: {
78
+ y: { display: false },
79
+ x: {
80
+ position: 'bottom',
81
+ display: true,
82
+ border: { display: false },
83
+ grid: { drawBorder: false, display: false },
84
+ ticks: {
85
+ padding: 8,
107
86
  },
108
- }
87
+ },
88
+ },
89
+ },
90
+ };
91
+
92
+ const visitorsData = [];
93
+
94
+ this.chart = new Chart(
95
+ this.element,
96
+ config,
97
+ );
98
+ }
99
+
100
+ formatLabel(label) {
101
+ return label
102
+ }
103
+
104
+ formatMetric(metric) {
105
+ return metric
106
+ }
107
+
108
+
109
+ extractTooltipData(tooltip) {
110
+ const data = this.funnel.steps.find(step => step.name === tooltip.title[0]);
109
111
 
110
- const visitorsData = []
112
+ const value = data.total_events;
113
+ const label = "Visitors"
114
+ let comparisonLabel = "Dropoff"
115
+ let comparisonValue = data.unique_visits;
111
116
 
112
- new Chart(
113
- this.element,
114
- config
115
- );
117
+ return {
118
+ comparison: true,
119
+ comparisonDifference: false,
120
+ metric: tooltip.title[0],
121
+ label: this.formatLabel(label),
122
+ labelBackgroundColor: getCSS('--bc'),
123
+ formattedValue: value,
124
+ comparisonLabel: comparisonLabel,
125
+ comparisonLabelBackgroundColor: "",
126
+ formattedComparisonValue: comparisonValue,
116
127
  }
128
+ }
117
129
  }
@@ -1,3 +1,4 @@
1
- import {application} from "controllers/application"
2
- import {eagerLoadControllersFrom} from "@hotwired/stimulus-loading"
3
- eagerLoadControllersFrom("controllers", application)
1
+ import { application } from 'controllers/application';
2
+ import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading';
3
+
4
+ eagerLoadControllersFrom('controllers', application);
@@ -1,10 +1,15 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  handleChange(event) {
5
5
  const url = new URL(event.target.form.action);
6
6
  const interval = event.target.value;
7
- url.searchParams.set("interval", interval)
8
- event.target.closest('turbo-frame').src = url.href
7
+ url.searchParams.set('interval', interval);
8
+ event.target.closest('turbo-frame').src = url.href;
9
+ document.querySelectorAll('a[data-turbo-frame="chart"]').forEach(el => {
10
+ const url = new URL(el.href);
11
+ url.searchParams.set('interval', interval);
12
+ el.href = url.href
13
+ })
9
14
  }
10
15
  }
@@ -0,0 +1,188 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { getCSS, externalTooltipHandler, dateFormatter, metricFormatter } from 'helpers/chart_utils';
3
+
4
+ const calculatePercentageDifference = function(oldValue, newValue) {
5
+ if(!oldValue) { return false }
6
+ if (oldValue == 0 && newValue > 0) {
7
+ return 100
8
+ } else if (oldValue == 0 && newValue == 0) {
9
+ return 0
10
+ } else {
11
+ return Math.round((newValue - oldValue) / oldValue * 100)
12
+ }
13
+ }
14
+
15
+ const footer = (tooltipItems) => {
16
+ let sum = 0;
17
+
18
+ tooltipItems.forEach(function(tooltipItem) {
19
+ sum += tooltipItem.parsed.y;
20
+ });
21
+ return 'Sum: ' + sum;
22
+ };
23
+
24
+ export default class extends Controller {
25
+ static values = {
26
+ current: Object,
27
+ comparedTo: Object,
28
+ interval: String,
29
+ label: String,
30
+ metric: String,
31
+ comparison: String
32
+ }
33
+
34
+ connect() {
35
+ const onClick = (e) => {
36
+ const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0];
37
+ const searchParams = new URLSearchParams(window.location.search);
38
+
39
+ searchParams.delete('period')
40
+ searchParams.delete('start_date')
41
+ searchParams.delete('end_date')
42
+ searchParams.delete('compare_to_start_date')
43
+ searchParams.delete('compare_to_end_date')
44
+ searchParams.set('date', Object.keys(this.currentValue)[element.index])
45
+
46
+ Turbo.visit(window.location.pathname + "?" + searchParams.toString())
47
+ }
48
+ const datasets = [
49
+ {
50
+ label: Object.keys(this.currentValue),
51
+ data: Object.values(this.currentValue),
52
+ borderColor: getCSS('--a'),
53
+ backgroundColor: getCSS('--a'),
54
+ color: getCSS('--bc'),
55
+ yAxisID: 'y',
56
+ }
57
+ ]
58
+
59
+ if(this.hasComparedToValue) {
60
+ datasets.push({
61
+ label: Object.keys(this.comparedToValue),
62
+ data: Object.values(this.comparedToValue),
63
+ borderColor: getCSS('--s', 0.8),
64
+ backgroundColor: getCSS('--s', 0.8),
65
+ color: getCSS('--bc'),
66
+ yAxisID: 'yComparison',
67
+ })
68
+ }
69
+
70
+ const calculateMaximumY = function(dataset) {
71
+ if (dataset) {
72
+ return Math.max(Object.values(dataset))
73
+ } else {
74
+ return 1
75
+ }
76
+ }
77
+
78
+ const typeForDate = this.comparisonValue === 'year' ? 'long' : "short"
79
+ this.chart = new Chart(this.element,
80
+ {
81
+ type: 'line',
82
+ data: {
83
+ labels: Object.keys(this.currentValue),
84
+ datasets: datasets
85
+ },
86
+ options: {
87
+ onClick: onClick,
88
+ responsive: true,
89
+ maintainAspectRatio: false,
90
+ interaction: {
91
+ intersect: false,
92
+ mode: 'index',
93
+ },
94
+ plugins: {
95
+ legend: false,
96
+ tooltip: {
97
+ enabled: false,
98
+ position: 'nearest',
99
+ external: externalTooltipHandler(this)
100
+ }
101
+ },
102
+ scales: {
103
+ y: {
104
+ min: 0,
105
+ suggestedMax: calculateMaximumY(this.currentValue),
106
+ ticks: {
107
+ },
108
+ grid: {
109
+ zeroLineColor: 'transparent',
110
+ drawBorder: false,
111
+ }
112
+ },
113
+ yComparison: {
114
+ min: 0,
115
+ suggestedMax: calculateMaximumY(this.currentValue),
116
+ display: false,
117
+ grid: { display: false },
118
+ },
119
+ x: {
120
+ ticks: {
121
+ grid: { display: false },
122
+
123
+ color: getCSS('--bc'),
124
+ callback: (val, idx) => {
125
+ if(idx % 2 == 0) {
126
+ const date = Object.keys(this.currentValue)[val];
127
+ return dateFormatter[this.intervalValue](date, typeForDate)
128
+ } else {
129
+ return ""
130
+ }
131
+
132
+ }
133
+ }
134
+ }
135
+ },
136
+ elements: {
137
+ point: {
138
+ radius: 0
139
+ }
140
+ }
141
+ }
142
+ },
143
+ );
144
+ }
145
+
146
+ formatLabel(label) {
147
+ return dateFormatter[this.intervalValue](label, 'long')
148
+ }
149
+
150
+ formatMetric(value) {
151
+ return metricFormatter[this.metricValue](value)
152
+ }
153
+
154
+ resize() { this.chart.resize() };
155
+
156
+ extractTooltipData(tooltip) {
157
+ const data = this.chart.config.data.datasets.find((set) => set.yAxisID == "y")
158
+ const comparisonData = this.chart.config.data.datasets.find((set) => set.yAxisID == "yComparison");
159
+ const dataIndex = this.chart.config.data.datasets.indexOf(data)
160
+ const comparisonDataIndex = this.chart.config.data.datasets.indexOf(comparisonData);
161
+
162
+ const tooltipData = tooltip.dataPoints.find((dataPoint) => dataPoint.datasetIndex == dataIndex)
163
+ const label = data.label[tooltipData.dataIndex];
164
+ let comparisonLabel = false
165
+ let comparisonValue = false
166
+ let comparisonLabelBackgroundColor = false
167
+ if(this.hasComparedToValue) {
168
+ const tooltipComparisonData = tooltip.dataPoints.find((dataPoint) => dataPoint.datasetIndex == comparisonDataIndex);
169
+ comparisonLabel = comparisonData.label[tooltipComparisonData.dataIndex];
170
+ comparisonValue = tooltip.dataPoints.find((dataPoint) => dataPoint.datasetIndex == comparisonDataIndex)?.raw || 0
171
+ comparisonLabelBackgroundColor = comparisonData.backgroundColor
172
+ }
173
+
174
+ const value = tooltip.dataPoints.find((dataPoint) => dataPoint.datasetIndex == dataIndex)?.raw || 0
175
+
176
+ return {
177
+ comparison: this.hasComparedToValue,
178
+ comparisonDifference: calculatePercentageDifference(comparisonValue, value),
179
+ metric: this.labelValue,
180
+ label: this.formatLabel(label),
181
+ labelBackgroundColor: data.backgroundColor,
182
+ formattedValue: this.formatMetric(value),
183
+ comparisonLabel: this.formatLabel(comparisonLabel),
184
+ comparisonLabelBackgroundColor: comparisonLabelBackgroundColor,
185
+ formattedComparisonValue: this.formatMetric(comparisonValue)
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,9 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["select"]
5
+
6
+ handleChange(event) {
7
+ this.selectTarget.name = event.target.value
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ handleChange(event) {
6
+ document.querySelector('turbo-frame#goals').src = event.target.value
7
+ }
8
+ }
@@ -0,0 +1,46 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ['name', 'value'];
5
+
6
+ connect() {
7
+ this.init = this.init.bind(this)
8
+
9
+ const interval = setInterval(() => {
10
+ if(window.comboboxConnected === 2) {
11
+ clearInterval(interval);
12
+ this.init()
13
+ }
14
+ }, 100)
15
+
16
+ }
17
+
18
+ init() {
19
+
20
+ if(this.nameTarget.value) {
21
+ this.valueTarget.combobox.element.dataset.comboboxQueryValue = `q[properties.${this.nameTarget.value}_i_cont]`
22
+ }
23
+
24
+ this.nameTarget.addEventListener("change", (event) => {
25
+ if(event.target.value) {
26
+ event.target.dataset.column = event.target.value;
27
+ this.valueTarget.combobox.element.dataset.comboboxQueryValue = `q[properties.${event.target.value}_i_cont]`
28
+ this.valueTarget.combobox.setDisabled(false)
29
+ this.valueTarget.combobox.isOpenValue = false
30
+ } else {
31
+ this.valueTarget.combobox.setSelected([])
32
+ this.valueTarget.combobox.setDisabled(true)
33
+ }
34
+ })
35
+ this.valueTarget.addEventListener("change", (event) => {
36
+ if(event.target.value.length > 0) {
37
+ this.valueTarget.name = `q[properties.${this.nameTarget.dataset.column}_in]`
38
+ } else {
39
+ this.valueTarget.name = null
40
+ this.valueTarget.combobox.element.dataset.comboboxQueryValue = ""
41
+ }
42
+ })
43
+
44
+ }
45
+
46
+ }
@@ -1,13 +1,16 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["label"]
5
-
4
+ static targets = ['label'];
5
+ static values = {
6
+ interval: Number
7
+ }
6
8
  connect() {
7
- this.reload = this.reload.bind(this)
8
- this.setLabel = this.setLabel.bind(this)
9
+ this.reload = this.reload.bind(this);
10
+ this.setLabel = this.setLabel.bind(this);
9
11
  this.labelCount = 0;
10
-
12
+ this.reloadInterval = setInterval(this.reload, 1000 * this.intervalValue);
13
+ this.labelInterval = setInterval(this.setLabel, 1000);
11
14
  }
12
15
 
13
16
  reload() {
@@ -17,11 +20,11 @@ export default class extends Controller {
17
20
 
18
21
  setLabel() {
19
22
  this.labelTarget.title = `Last updated ${this.labelCount} seconds ago`;
20
- this.labelCount += 1
23
+ this.labelCount += 1;
21
24
  }
22
25
 
23
26
  disconnect() {
24
- clearInterval(this.labelInterval)
25
- clearInterval(this.reloadInterval)
27
+ clearInterval(this.labelInterval);
28
+ clearInterval(this.reloadInterval);
26
29
  }
27
30
  }
@@ -0,0 +1,9 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ["title"]
5
+ setTitle(event) {
6
+ this.titleTarget.innerHTML = event.target.title || event.target.text
7
+ }
8
+
9
+ }
@@ -0,0 +1,17 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Connects to data-controller="toggle"
4
+ export default class extends Controller {
5
+ static targets = ['toggleable'];
6
+ static values = {
7
+ enable: Boolean
8
+ }
9
+
10
+ trigger() {
11
+ if (this.enableValue) {
12
+ this.toggleableTargets.forEach(element => {
13
+ element.classList.toggle('hidden');
14
+ });
15
+ }
16
+ }
17
+ }