active_analytics 0.2.0 → 0.3.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +105 -15
  3. data/app/controllers/active_analytics/application_controller.rb +24 -4
  4. data/app/controllers/active_analytics/assets_controller.rb +29 -0
  5. data/app/controllers/active_analytics/pages_controller.rb +9 -10
  6. data/app/controllers/active_analytics/referrers_controller.rb +9 -5
  7. data/app/controllers/active_analytics/sites_controller.rb +4 -4
  8. data/app/models/active_analytics/views_per_day.rb +22 -3
  9. data/app/views/active_analytics/assets/_charts.css +249 -0
  10. data/app/{assets/stylesheets/active_analytics/style.css → views/active_analytics/assets/_style.css} +37 -38
  11. data/app/views/active_analytics/assets/application.css.erb +2 -0
  12. data/app/{assets/javascripts/active_analytics → views/active_analytics/assets}/application.js +1 -3
  13. data/app/views/active_analytics/assets/ariato.css +875 -0
  14. data/app/views/active_analytics/assets/ariato.js +322 -0
  15. data/app/views/active_analytics/pages/_table.html.erb +6 -17
  16. data/app/views/active_analytics/pages/index.html.erb +1 -1
  17. data/app/views/active_analytics/pages/show.html.erb +7 -8
  18. data/app/views/active_analytics/referrers/_table.html.erb +9 -2
  19. data/app/views/active_analytics/referrers/index.html.erb +2 -2
  20. data/app/views/active_analytics/referrers/show.html.erb +6 -3
  21. data/app/views/active_analytics/sites/_histogram.html.erb +9 -2
  22. data/app/views/active_analytics/sites/_histogram_header.html.erb +10 -0
  23. data/app/views/active_analytics/sites/show.html.erb +2 -2
  24. data/app/views/layouts/active_analytics/_footer.html.erb +3 -3
  25. data/app/views/layouts/active_analytics/_header.html.erb +1 -3
  26. data/app/views/layouts/active_analytics/application.html.erb +5 -3
  27. data/config/routes.rb +2 -1
  28. data/lib/active_analytics/version.rb +1 -1
  29. data/lib/active_analytics.rb +47 -4
  30. metadata +15 -11
  31. data/app/assets/javascripts/active_analytics/ariato.js +0 -746
  32. data/app/assets/stylesheets/active_analytics/application.css +0 -15
  33. data/app/assets/stylesheets/active_analytics/ariato.css +0 -3548
  34. data/app/assets/stylesheets/active_analytics/charts.css +0 -424
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5054460d87d609cd11f9418dd84963ff89407d20c22beb685b6d2a9eb8b2f922
4
- data.tar.gz: 3b2b67cb7d9ce4e652bd22ea519fa294f05fe3b105bd3bfbae932e477a3f9fe2
3
+ metadata.gz: 909ce5d6ab8a28322be42f01b2d37c752ef810a31a139b7f5e035257d1c62fda
4
+ data.tar.gz: 1ca79cce7bbe415afa9341e752400891d6817e780a0b679fadcfeb1df6039786
5
5
  SHA512:
6
- metadata.gz: 2280cf59a6a021c902451bf8a51b7ad678f57917aad786d16a4eeebfc2931766c49f99081eba4d13e0a74fbeba711b26c9c3c12f9405edc20dee4902908f6077
7
- data.tar.gz: 915bd565e0218737fc98ce6a97948896be7beb65669a3c8fb5d29d1936f11e8476ad1c24324967ce9fccb7c0db65ebab74c5c31d0d582d7aa0d03dbe7099fb52
6
+ metadata.gz: 1835fa3cb335651ef42c46767b13da30829fb86a421f06989c203814c4b9199d784f714e4bf6885a4301befb4dae9fd1bad94b47fa660f12bd2425de4c713b51
7
+ data.tar.gz: 19fa332fbbdbc9be99e99b9a635bc57df3e2a32c4b9c0a12157c2e44446d3417eec0d702790c2a05b803858bafc50090d31ba787d5bb5e7cd52c87a4795c7e99
data/README.md CHANGED
@@ -32,43 +32,133 @@ rails active_analytics:install:migrations
32
32
  rails db:migrate
33
33
  ```
34
34
 
35
- Your controllers have to call `ActiveAnalytics.record_request(request)` to record page views:
35
+ Add the route to ActiveAnalytics dashboard at the desired endpoint:
36
+
37
+ ```ruby
38
+ # config/routes.rb
39
+ mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics
40
+ ```
41
+
42
+ The next step is to collect trafic and there is 2 options.
43
+
44
+ ### Record requests synchronously
45
+
46
+ This is the easiest way to start with.
47
+ However it's less performant since it triggers a write into your database for each request.
48
+ Your controllers have to call `ActiveAnalytics.record_request(request)` to record page views.
49
+ The Rails way to achieve is to use `after_action` :
50
+
36
51
  ```ruby
37
52
  class ApplicationController < ActionController::Base
38
- before_action :record_page_view
53
+ after_action :record_page_view
39
54
 
40
55
  def record_page_view
41
- # Add a condition to record only your canonical domain
42
- # and use a gem such as crawler_detect to skip bots.
43
- ActiveAnalytics.record_request(request)
56
+ # This is a basic example, you might need to customize some conditions.
57
+ # For most sites, it makes no sense to record anything other than HTML.
58
+ if response.content_type && response.content_type.start_with?("text/html")
59
+ # Add a condition to record only your canonical domain
60
+ # and use a gem such as crawler_detect to skip bots.
61
+ ActiveAnalytics.record_request(request)
62
+ end
44
63
  end
45
64
  end
46
65
  ```
47
66
 
48
- This is a basic `before_action`. In case you don't want to record all page views, simply define a `skip_before_action :record_page_view` in the relevant controller.
67
+ In case you don't want to record all page views, because each application has sensitive URLs such as password reset and so on, simply define a `skip_after_action :record_page_view` in the relevant controller.
68
+
69
+ ### Queue requests asynchronously
70
+
71
+ It requires more work and it's relevant if your application handle a large trafic.
72
+ The idea is to queue data into Redis because it does not require the database writing to the disk on each request.
73
+ First you have to set the Redis URL or connection.
49
74
 
50
- Finally, just add the route to ActiveAnalytics dashboard at the desired endpoint:
51
75
  ```ruby
52
- mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics
76
+ # File lib/patches/active_analytics.rb or config/initializers/active_analytics.rb
77
+
78
+ ActiveAnalytics.redis_url = "redis://user:password@host/1" # Default ENV["ACTIVE_ANALYTICS_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost"
79
+
80
+ # If you use special connection options you have to instantiate it yourself
81
+ ActiveAnalytics.redis = Redis.new(
82
+ url: ENV["REDIS_URL"],
83
+ reconnect_attempts: 10,
84
+ ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE}
85
+ )
53
86
  ```
54
87
 
88
+ Then your controllers have to call `ActiveAnalytics.queue_request(request)` to queue page views.
89
+ The Rails way to achieve is to use `after_action` :
90
+
91
+ ```ruby
92
+ class ApplicationController < ActionController::Base
93
+ after_action :queue_page_view
94
+
95
+ def queue_page_view
96
+ # This is a basic example, you might need to customize some conditions.
97
+ # For most sites, it makes no sense to record anything other than HTML.
98
+ if response.content_type && response.content_type.start_with?("text/html")
99
+ # Add a condition to record only your canonical domain
100
+ # and use a gem such as crawler_detect to skip bots.
101
+ ActiveAnalytics.queue_request(request)
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
107
+ Queued data need to be saved into the database in order to be viewable in the ActiveAnalytics dashboard.
108
+ For that, call `ActiveAnalytics.flush_queue` from a cron task or a background job.
109
+
110
+ It's up to you if you want to flush the queue every hour or every 10 minutes.
111
+ I advise to execute the last flush of the day at 23:59.
112
+ It prevents from shifting the trafic to the next day.
113
+ In that case only the last minute will be shifted to the next day, even if the flush ends after midnight.
114
+ This small imperfection allows a simpler implementation for now.
115
+ Keep it simple !
116
+
117
+
55
118
  ## Authentication and permissions
56
- ActiveAnalytics cannot guess how you handle user authentication, because it is different for all Rails applications. So you have to inject your own mechanism into `ActiveAnalytics::ApplicationController`. Create a file in `config/initializers/active_analytics.rb`:
119
+
120
+ ActiveAnalytics cannot guess how you handle user authentication, because it is different for all Rails applications.
121
+ So you have to monkey patch `ActiveAnalytics::ApplicationController` in order to inject your own mechanism.
122
+ The patch can be saved wherever you want.
123
+ For example, I like to have all the patches in one place, so I put them in `lib/patches`.
57
124
 
58
125
  ```ruby
59
- require_dependency "active_analytics/application_controller"
126
+ # lib/patches/active_analytics.rb
127
+
128
+ ActiveAnalytics::ApplicationController.class_eval do
129
+ before_action :require_admin
60
130
 
61
- module ActiveAnalytics
62
- class ApplicationController
63
- # include Currentuser # This is an example that you have to change by
64
- # before_action :require_admin # your own modules and methods
131
+ def require_admin
132
+ # This example supposes there are current_user and User#admin? methods
133
+ raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?)
134
+ end
65
135
  end
66
136
  end
67
137
  ```
68
138
 
139
+ Then you have to require the monkey patch.
140
+ Because it's loaded via require, it won't be reloaded in development.
141
+ Since you are not supposed to change this file often, it should not be an issue.
142
+
143
+ ```ruby
144
+ # config/application.rb
145
+ config.after_initialize do
146
+ require "patches/active_analytics"
147
+ end
148
+ ```
149
+
150
+ If you use Devise, you can check the permission directly from routes.rb :
151
+
152
+ ```ruby
153
+ # config/routes.rb
154
+ authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method
155
+ mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics
156
+ end
157
+ ```
158
+
69
159
  ## License
70
160
  The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
71
161
 
72
162
  Made by [Base Secrète](https://basesecrete.com).
73
163
 
74
- Rails developer? Check out [RoRvsWild](https://rorvswild.com), our Ruby on Rails application monitoring tool.
164
+ Rails developer? Check out [RoRvsWild](https://rorvswild.com), our Ruby on Rails application monitoring tool.
@@ -3,14 +3,34 @@ module ActiveAnalytics
3
3
 
4
4
  private
5
5
 
6
+ def from_date
7
+ @from_date ||= Date.parse(params[:from])
8
+ end
9
+
10
+ def to_date
11
+ @to_date ||= Date.parse(params[:to])
12
+ end
13
+
6
14
  def require_date_range
7
- if params[:from].blank? || params[:to].blank?
8
- redirect_to(params.to_unsafe_hash.merge(from: 7.days.ago.to_date, to: Date.today))
9
- end
15
+ redirect_to(params.to_unsafe_hash.merge(from: to_date, to: from_date)) if from_date > to_date
16
+ rescue TypeError, ArgumentError # Raised by Date.parse when invalid format
17
+ redirect_to(params.to_unsafe_hash.merge(from: 7.days.ago.to_date, to: Date.today))
10
18
  end
11
19
 
12
20
  def current_views_per_days
13
- ViewsPerDay.where(site: params[:site]).between_dates(params[:from], params[:to])
21
+ ViewsPerDay.where(site: params[:site]).between_dates(from_date, to_date)
22
+ end
23
+
24
+ def previous_views_per_days
25
+ ViewsPerDay.where(site: params[:site]).between_dates(previous_from_date, previous_to_date)
26
+ end
27
+
28
+ def previous_from_date
29
+ from_date - (to_date - from_date)
30
+ end
31
+
32
+ def previous_to_date
33
+ to_date - (to_date - from_date)
14
34
  end
15
35
  end
16
36
  end
@@ -0,0 +1,29 @@
1
+ require_dependency "active_analytics/application_controller"
2
+
3
+ module ActiveAnalytics
4
+ class AssetsController < ApplicationController
5
+ protect_from_forgery except: :show
6
+
7
+ def show
8
+ if endpoints.include?(File.basename(request.path))
9
+ expires_in(1.day, public: true)
10
+ render(params[:id], mime_type: mime_type)
11
+ else
12
+ raise ActionController::RoutingError.new
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def endpoints
19
+ return @endpoints if @endpoints
20
+ folder = ActiveAnalytics::Engine.root.join("app/views", controller_path)
21
+ files = folder.each_child.map { |path| File.basename(path).delete_suffix(".erb") }
22
+ @endpoints = files.delete_if { |str| str.start_with?("_") }
23
+ end
24
+
25
+ def mime_type
26
+ Mime::Type.lookup_by_extension(File.extname(request.path).delete_prefix("."))
27
+ end
28
+ end
29
+ end
@@ -7,19 +7,18 @@ module ActiveAnalytics
7
7
  before_action :require_date_range
8
8
 
9
9
  def index
10
- scope = ViewsPerDay.where(site: params[:site]).between_dates(params[:from], params[:to])
11
- @histogram = ViewsPerDay::Histogram.new(scope.order_by_date.group_by_date, params[:from], params[:to])
12
- @pages = scope.top(100).group_by_page
10
+ @histogram = ViewsPerDay::Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
11
+ @previous_histogram = ViewsPerDay::Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
12
+ @pages = current_views_per_days.top(100).group_by_page
13
13
  end
14
14
 
15
15
  def show
16
- dates_scopes = ViewsPerDay.between_dates(params[:from], params[:to])
17
- page_scope = dates_scopes.where(site: params[:site], page: page_from_params)
18
- @histogram = ViewsPerDay::Histogram.new(page_scope.order_by_date.group_by_date, params[:from], params[:to])
19
- @referrers = page_scope.top.group_by_referrer_site
20
-
21
- @next_pages = dates_scopes.where(referrer_host: params[:site], referrer_path: page_from_params).top(100).group_by_page
22
- @previous_pages = dates_scopes.where(site: params[:site], page: page_from_params).where.not(referrer_path: nil).top(100).group_by_referrer_page
16
+ page_scope = current_views_per_days.where(page: page_from_params)
17
+ previous_page_scope = previous_views_per_days.where(page: page_from_params)
18
+ @histogram = ViewsPerDay::Histogram.new(page_scope.order_by_date.group_by_date, from_date, to_date)
19
+ @previous_histogram = ViewsPerDay::Histogram.new(previous_page_scope.order_by_date.group_by_date, previous_from_date, previous_to_date)
20
+ @next_pages = current_views_per_days.where(referrer_host: params[:site], referrer_path: page_from_params).top(100).group_by_page
21
+ @previous_pages = page_scope.top(100).group_by_referrer_page
23
22
  end
24
23
  end
25
24
  end
@@ -5,14 +5,18 @@ module ActiveAnalytics
5
5
  before_action :require_date_range
6
6
 
7
7
  def index
8
- scope = ViewsPerDay.where(site: params[:site]).between_dates(params[:from], params[:to])
9
- @referrers = scope.top(100).group_by_referrer_site
10
- @histogram = ViewsPerDay::Histogram.new(scope.order_by_date.group_by_date, params[:from], params[:to])
8
+ @referrers = current_views_per_days.top(100).group_by_referrer_site
9
+ @histogram = ViewsPerDay::Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
10
+ @previous_histogram = ViewsPerDay::Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
11
11
  end
12
12
 
13
13
  def show
14
- scope = ViewsPerDay.where(site: params[:site], referrer_host: params[:referrer]).between_dates(params[:from], params[:to])
15
- @histogram = ViewsPerDay::Histogram.new(scope.order_by_date.group_by_date, params[:from], params[:to])
14
+ referrer_host, referrer_path = params[:referrer].split("/", 2)
15
+ scope = current_views_per_days.where(referrer_host: referrer_host)
16
+ scope = scope.where(referrer_path: "/" + referrer_path) if referrer_path.present?
17
+ previous_scope = previous_views_per_days.where(referrer_host: params[:referrer])
18
+ @histogram = ViewsPerDay::Histogram.new(scope.order_by_date.group_by_date, from_date, to_date)
19
+ @previous_histogram = ViewsPerDay::Histogram.new(previous_scope.order_by_date.group_by_date, previous_from_date, previous_to_date)
16
20
  @previous_pages = scope.top(100).group_by_referrer_page
17
21
  @next_pages = scope.top(100).group_by_page
18
22
  end
@@ -10,10 +10,10 @@ module ActiveAnalytics
10
10
  end
11
11
 
12
12
  def show
13
- scope = current_views_per_days
14
- @histogram = ViewsPerDay::Histogram.new(scope.order_by_date.group_by_date, params[:from], params[:to])
15
- @referrers = scope.top.group_by_referrer_site
16
- @pages = scope.top.group_by_page
13
+ @histogram = ViewsPerDay::Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
14
+ @previous_histogram = ViewsPerDay::Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
15
+ @referrers = current_views_per_days.top.group_by_referrer_site
16
+ @pages = current_views_per_days.top.group_by_page
17
17
  end
18
18
  end
19
19
  end
@@ -37,8 +37,10 @@ module ActiveAnalytics
37
37
  attr_reader :bars, :from_date, :to_date
38
38
 
39
39
  def initialize(scope, from_date, to_date)
40
+ @scope = scope
41
+ @from_date, @to_date = from_date, to_date
40
42
  @bars = scope.map { |day| Bar.new(day.day, day.total, self) }
41
- fill_missing_days(@bars, Date.parse(from_date.to_s), Date.parse(to_date.to_s))
43
+ fill_missing_days(@bars, @from_date, @to_date)
42
44
  end
43
45
 
44
46
  def fill_missing_days(bars, from, to)
@@ -68,7 +70,11 @@ module ActiveAnalytics
68
70
  end
69
71
 
70
72
  def height
71
- (value.to_f / histogram.max_value).round(2)
73
+ if histogram.max_value > 0
74
+ (value.to_f / histogram.max_value).round(2)
75
+ else
76
+ 0
77
+ end
72
78
  end
73
79
  end
74
80
  end
@@ -108,12 +114,25 @@ module ActiveAnalytics
108
114
  end
109
115
 
110
116
  def self.append(params)
117
+ total = params.delete(:total) || 1
111
118
  params[:site] = params[:site].downcase if params[:site]
112
119
  params[:page] = params[:page].downcase if params[:page]
113
120
  params[:referrer_path] = nil if params[:referrer_path].blank?
114
121
  params[:referrer_path] = params[:referrer_path].downcase if params[:referrer_path]
115
122
  params[:referrer_host] = params[:referrer_host].downcase if params[:referrer_host]
116
- find_or_create_by!(params) if where(params).update_all("total = total + 1") == 0
123
+ where(params).first.try(:increment!, :total, total) || create!(params.merge(total: total))
124
+ end
125
+
126
+ SLASH = "/"
127
+
128
+ def self.split_referrer(referrer)
129
+ return [nil, nil] if referrer.blank?
130
+ if (uri = URI(referrer)).host.present?
131
+ [uri.host, uri.path.presence]
132
+ else
133
+ strings = referrer.split(SLASH, 2)
134
+ [strings[0], strings[1] ? SLASH + strings[1] : nil]
135
+ end
117
136
  end
118
137
  end
119
138
  end
@@ -0,0 +1,249 @@
1
+ /*
2
+ * Charts.css v0.9.0 (https://ChartsCSS.org/)
3
+ * Copyright 2020 Rami Yushuvaev
4
+ * Licensed under MIT
5
+ */
6
+ .active-analytics .charts-css {
7
+ --chart-bg-color: transparent;
8
+ --heading-size: 0px;
9
+ --primary-axis-color: rgba(var(--color-grey-100), 1);
10
+ --primary-axis-style: solid;
11
+ --primary-axis-width: 1px;
12
+ --secondary-axes-color: rgba(var(--color-grey-50), 1);
13
+ --secondary-axes-style: solid;
14
+ --secondary-axes-width: 1px;
15
+ --data-axes-color: rgba(var(--color-grey-200), 1);
16
+ --data-axes-style: solid;
17
+ --data-axes-width: 1px;
18
+ --legend-border-color: rgba(var(--color-grey-200), 1);
19
+ position: relative;
20
+ display: block;
21
+ margin: 0;
22
+ padding: 0;
23
+ border: 0;
24
+ }
25
+
26
+ /*
27
+ * Chart wrapper element
28
+ */
29
+
30
+ .active-analytics .charts-css,
31
+ .active-analytics .charts-css::after,
32
+ .active-analytics .charts-css::before,
33
+ .active-analytics .charts-css *,
34
+ .active-analytics .charts-css *::after,
35
+ .active-analytics .charts-css *::before {
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ /*
40
+ * Reset table element
41
+ */
42
+ .active-analytics table.charts-css {
43
+ border-collapse: collapse;
44
+ border-spacing: 0;
45
+ empty-cells: show;
46
+ overflow: initial;
47
+ background-color: transparent;
48
+ }
49
+
50
+ .active-analytics table.charts-css caption,
51
+ .active-analytics table.charts-css colgroup,
52
+ .active-analytics table.charts-css thead,
53
+ .active-analytics table.charts-css tbody,
54
+ .active-analytics table.charts-css tr,
55
+ .active-analytics table.charts-css th,
56
+ .active-analytics table.charts-css td {
57
+ display: block;
58
+ margin: 0;
59
+ padding: 0;
60
+ border: 0;
61
+ background-color: transparent;
62
+ }
63
+
64
+ .active-analytics table.charts-css colgroup,
65
+ .active-analytics table.charts-css thead,
66
+ .active-analytics table.charts-css tfoot {
67
+ display: none;
68
+ }
69
+
70
+
71
+ /*
72
+ * Chart colors
73
+ */
74
+
75
+ .active-analytics .charts-css.column tbody tr td {
76
+ background: rgba(var(--color-grey-100), 1);
77
+ padding: 0;
78
+ }
79
+
80
+ /*
81
+ * Chart data
82
+ */
83
+ .active-analytics .charts-css.hide-data .data {
84
+ opacity: 0;
85
+ }
86
+
87
+ .active-analytics .charts-css.show-data-on-hover .data {
88
+ transition-duration: .3s;
89
+ opacity: 0;
90
+ }
91
+
92
+ .active-analytics .charts-css.show-data-on-hover tr:hover .data {
93
+ transition-duration: .3s;
94
+ opacity: 1;
95
+ }
96
+
97
+ /*
98
+ * Chart labels
99
+ */
100
+
101
+ .active-analytics .charts-css.column:not(.show-labels) {
102
+ --labels-size: 0;
103
+ }
104
+
105
+ .active-analytics .charts-css.column:not(.show-labels) tbody tr th {
106
+ display: none;
107
+ }
108
+
109
+ .active-analytics .charts-css.column.show-labels {
110
+ --labels-size: 1.5rem;
111
+ }
112
+
113
+ .active-analytics .charts-css.column.show-labels tbody tr th {
114
+ display: flex;
115
+ justify-content: var(--labels-align, center);
116
+ align-items: center;
117
+ flex-direction: column;
118
+ }
119
+
120
+ @media (max-width: 600px) {
121
+ .active-analytics .charts-css.column.show-labels {
122
+ --labels-size: 0;
123
+ }
124
+
125
+ .active-analytics .charts-css.column.show-labels tbody tr th {
126
+ display: none;
127
+ }
128
+ }
129
+
130
+ /*
131
+ * Chart axes
132
+ */
133
+ .active-analytics .charts-css.column.show-primary-axis:not(.reverse) tbody tr {
134
+ border-block-end: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);
135
+ }
136
+
137
+ .active-analytics .charts-css.column.show-primary-axis.reverse tbody tr {
138
+ border-block-start: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);
139
+ }
140
+
141
+ .active-analytics .charts-css.column.show-5-secondary-axes:not(.reverse) tbody tr {
142
+ background-size: 100% 20%;
143
+ background-image: linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width));
144
+ }
145
+
146
+ .active-analytics .charts-css.column.show-5-secondary-axes.reverse tbody tr {
147
+ background-size: 100% 20%;
148
+ background-image: linear-gradient(0deg, var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width));
149
+ }
150
+
151
+ /*
152
+ * Chart tooltips
153
+ */
154
+ .active-analytics .charts-css .tooltip {
155
+ position: absolute;
156
+ z-index: 1;
157
+ bottom: 50%;
158
+ left: 50%;
159
+ transform: translateX(-50%);
160
+ width: max-content;
161
+ padding: 5px 10px;
162
+ border-radius: 6px;
163
+ visibility: hidden;
164
+ opacity: 0;
165
+ transition: opacity .3s;
166
+ background-color: rgba(var(--color-grey-500), 1);
167
+ color: rgba(var(--color-grey-00), 1);
168
+ text-align: center;
169
+ font-size: .9rem;
170
+ }
171
+
172
+ .active-analytics .charts-css .tooltip::after {
173
+ content: "";
174
+ position: absolute;
175
+ top: 100%;
176
+ left: 50%;
177
+ margin-left: -5px;
178
+ border-width: 5px;
179
+ border-style: solid;
180
+ border-color: rgba(var(--color-grey-500), 1) transparent transparent;
181
+ }
182
+
183
+ .active-analytics .charts-css tr:hover .tooltip {
184
+ visibility: visible;
185
+ opacity: 1;
186
+ }
187
+
188
+ /*
189
+ * Column Chart
190
+ */
191
+ .active-analytics .charts-css.column tbody {
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: stretch;
195
+ width: 100%;
196
+ gap: 1px;
197
+ height: calc(100% - var(--heading-size));
198
+ }
199
+
200
+ .active-analytics .charts-css.column tbody tr {
201
+ position: relative;
202
+ flex-grow: 1;
203
+ flex-shrink: 1;
204
+ flex-basis: 0;
205
+ overflow-wrap: anywhere;
206
+ display: flex;
207
+ justify-content: flex-start;
208
+ min-width: 0;
209
+ }
210
+
211
+ .active-analytics .charts-css.column tbody tr th {
212
+ position: absolute;
213
+ right: 0;
214
+ left: 0;
215
+ }
216
+
217
+ .active-analytics .charts-css.column tbody tr td {
218
+ display: flex;
219
+ justify-content: center;
220
+ width: 100%;
221
+ height: calc(100% * var(--size, 1));
222
+ position: relative;
223
+ }
224
+
225
+ .active-analytics .charts-css.column:not(.reverse) tbody tr {
226
+ align-items: flex-end;
227
+ margin-block-end: var(--labels-size);
228
+ }
229
+
230
+ .active-analytics .charts-css.column:not(.reverse) tbody tr th {
231
+ bottom: calc(-1 * var(--labels-size) - var(--primary-axis-width));
232
+ height: var(--labels-size);
233
+ color: rgba(var(--color-grey-400), 1);
234
+ font-weight: 400;
235
+ }
236
+
237
+ .active-analytics .charts-css.column:not(.reverse) tbody tr td {
238
+ align-items: flex-start;
239
+ }
240
+
241
+ .active-analytics .charts-css.column:not(.stacked) tbody tr td {
242
+ flex-grow: 1;
243
+ flex-shrink: 1;
244
+ flex-basis: 0;
245
+ }
246
+
247
+ .active-analytics .charts-css.column:not(.reverse-data) tbody {
248
+ flex-direction: row;
249
+ }