active_analytics 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }