rails_analytics 1.0.4 → 1.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20ebdf5f4a45c3f8cad6c9e607be2d5eeaccb7669311c6f86b619aa2f76992dd
4
- data.tar.gz: 36b3978273cb8d054fec49443cc5f7ea5e3cb83c18a2c63f3a60a706dd402616
3
+ metadata.gz: c6a607ecb6f634b056bce7db997068d29e41ecc29c20d1fe38fb352a2e81b70d
4
+ data.tar.gz: 9ce65316e5f58c2c644df424315d29597d129560ead62e530c2499a1cbd2c26f
5
5
  SHA512:
6
- metadata.gz: 15d271dfdeae4344ed730fcc9bf467f962180e6093d219152d2d863f4556e6f1df113b9a91b0bab7b61e0011823a3b3a39890fd953905e7ecccf2e1a761758dc
7
- data.tar.gz: 9347e3775bec41f15af5a827ee148da0fd2fbbf91c3429b0cbf6807fc5da4f3208e1fed835a78ba79e9fabc44b66042aec0963d296a62ff6f5a1fec881a37c66
6
+ metadata.gz: 48c2a3920a2ffab6d0d59093608dd61607c6a3fabf1f1cb998b0acd368781473bdcd672e9a4d384b439123a9029912d9e519ef08e0e6624cd87721b5f4115541
7
+ data.tar.gz: c19d0b161aeeb89208cac8c59e975e21be96539cb7ac1adbd8a6e693fcd0461decce6a65338a04262fac527119438671e66f3e6d1eedb06c688450463765850f
data/README.md CHANGED
@@ -5,6 +5,10 @@ third-party analytics dependencies — it collects traffic **server-side**, stor
5
5
  your own database, and renders an aggregated dashboard with zero client-side JS
6
6
  dependencies.
7
7
 
8
+ The dashboard's chart shows CSS-only tooltips: hover (or keyboard-focus) any
9
+ data point to see its date and visit count — labels on the x-axis mark first,
10
+ last and month boundaries. No JavaScript anywhere.
11
+
8
12
  ## Screenshots
9
13
 
10
14
  | Dashboard — aggregated overview | Dashboard — events & visits |
@@ -66,6 +66,33 @@ body {
66
66
  .ra-chart rect { fill: var(--ra-accent); opacity: 0.85; }
67
67
  .ra-chart rect:hover { opacity: 1; }
68
68
 
69
+ /* Chart nodes: CSS-only tooltip (no JS). The tooltip is an SVG group
70
+ revealed on :hover/:focus-within of its parent .ra-dot group. */
71
+ .ra-chart .ra-dot { cursor: pointer; outline: none; }
72
+ .ra-chart .ra-dot-hit { pointer-events: all; }
73
+ .ra-chart .ra-dot:focus-visible .ra-dot-circle,
74
+ .ra-chart .ra-dot:focus .ra-dot-circle { stroke: var(--ra-text); stroke-width: 2; }
75
+
76
+ .ra-chart .ra-tooltip { opacity: 0; pointer-events: none; transition: opacity 0.12s ease; }
77
+ .ra-chart .ra-dot:hover .ra-tooltip,
78
+ .ra-chart .ra-dot:focus .ra-tooltip,
79
+ .ra-chart .ra-dot:focus-within .ra-tooltip { opacity: 1; }
80
+
81
+ .ra-chart .ra-tooltip rect { fill: var(--ra-text); opacity: 1; stroke: none; }
82
+ .ra-chart .ra-tooltip text {
83
+ fill: var(--ra-card);
84
+ font-size: 12px;
85
+ font-weight: 600;
86
+ font-variant-numeric: tabular-nums;
87
+ }
88
+
89
+ /* X-axis date labels */
90
+ .ra-chart .ra-axis-label {
91
+ fill: var(--ra-muted);
92
+ font-size: 10px;
93
+ font-variant-numeric: tabular-nums;
94
+ }
95
+
69
96
  .ra-grid {
70
97
  display: grid;
71
98
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
@@ -2,11 +2,29 @@
2
2
 
3
3
  module RailsAnalytics
4
4
  # SVG line chart. Server-rendered, zero JS.
5
+ #
6
+ # Each data point renders as a focusable group containing the visible dot,
7
+ # a larger invisible hit area and a CSS-only tooltip (shown on :hover and
8
+ # :focus). X-axis labels mark the first and last point plus month boundaries.
5
9
  class ChartBuilder
6
10
  WIDTH = 800
7
11
  HEIGHT = 220
8
12
  PAD = 8
9
13
  RIGHT_PAD = 40
14
+ DOT_RADIUS = 3
15
+ HIT_RADIUS = 12
16
+
17
+ # Tooltip geometry. The tooltip is part of the SVG (server-computed),
18
+ # revealed purely with CSS on :hover/:focus — no JavaScript involved.
19
+ TOOLTIP_WIDTH = 130
20
+ TOOLTIP_HEIGHT = 30
21
+ TOOLTIP_OFFSET = 10
22
+
23
+ # Plot area keeps HEIGHT; the viewBox gains a strip for x-axis labels.
24
+ AXIS_PAD = 20
25
+
26
+ # Minimum horizontal gap (px) between x-axis labels.
27
+ AXIS_MIN_GAP = 70
10
28
 
11
29
  def initialize(series)
12
30
  @series = series # Array<Hash> with :date and :count
@@ -16,31 +34,96 @@ module RailsAnalytics
16
34
  return empty_chart if @series.empty?
17
35
 
18
36
  max = [@series.map { |s| s[:count] }.max.to_f, 1].max
19
- points = @series.map.with_index do |s, i|
20
- x = pad(i)
21
- y = HEIGHT - PAD - ((s[:count] / max) * (HEIGHT - PAD * 2)).round
22
- "#{x.round(2)},#{y.round(2)}"
23
- end.join(" ")
37
+ nodes = @series.map.with_index { |s, i| dot_node(s, i, max) }.join("\n")
24
38
 
25
39
  <<~SVG
26
- <svg viewBox="0 0 #{WIDTH + RIGHT_PAD} #{HEIGHT}" role="img" aria-label="#{I18n.t('rails_analytics.chart.visits_per_day')}" xmlns="http://www.w3.org/2000/svg">
27
- <polyline points="#{points}" fill="none" stroke="var(--ra-accent, #3b82f6)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
28
- #{dots(points)}
40
+ <svg viewBox="0 0 #{WIDTH + RIGHT_PAD} #{HEIGHT + AXIS_PAD}" role="img" aria-label="#{I18n.t('rails_analytics.chart.visits_per_day')}" xmlns="http://www.w3.org/2000/svg">
41
+ <polyline points="#{points(max)}" fill="none" stroke="var(--ra-accent, #3b82f6)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
42
+ #{axis_labels}
43
+ #{nodes}
29
44
  </svg>
30
45
  SVG
31
46
  end
32
47
 
33
48
  private
34
49
 
35
- def pad(index)
50
+ def points(max)
51
+ @series.each_with_index.map do |s, i|
52
+ "#{x(i).round(2)},#{y(s[:count], max).round(2)}"
53
+ end.join(" ")
54
+ end
55
+
56
+ def dot_node(series, index, max)
57
+ cx = x(index).round(2)
58
+ cy = y(series[:count], max).round(2)
59
+ label = tooltip_text(series)
60
+
61
+ <<~NODE.strip
62
+ <g class="ra-dot" tabindex="0" role="img" aria-label="#{label}">
63
+ <circle cx="#{cx}" cy="#{cy}" r="#{HIT_RADIUS}" fill="transparent" pointer-events="all" />
64
+ <circle cx="#{cx}" cy="#{cy}" r="#{DOT_RADIUS}" class="ra-dot-circle" />
65
+ #{tooltip(cx, cy, label)}
66
+ </g>
67
+ NODE
68
+ end
69
+
70
+ def tooltip(cx, cy, label)
71
+ # Above the dot; flips to the far side when it would clip the top edge.
72
+ tx = (cx - TOOLTIP_WIDTH / 2).clamp(0, WIDTH + RIGHT_PAD - TOOLTIP_WIDTH)
73
+ ty = cy - DOT_RADIUS - TOOLTIP_OFFSET - TOOLTIP_HEIGHT
74
+ ty = cy + DOT_RADIUS + TOOLTIP_OFFSET if ty < 0
75
+
76
+ <<~TOOLTIP.strip
77
+ <g class="ra-tooltip">
78
+ <rect x="#{tx}" y="#{ty}" width="#{TOOLTIP_WIDTH}" height="#{TOOLTIP_HEIGHT}" rx="6" />
79
+ <text x="#{tx + TOOLTIP_WIDTH / 2}" y="#{ty + TOOLTIP_HEIGHT / 2 + 4}" text-anchor="middle">#{label}</text>
80
+ </g>
81
+ TOOLTIP
82
+ end
83
+
84
+ def tooltip_text(series)
85
+ I18n.t("rails_analytics.chart.tooltip", count: series[:count],
86
+ date: series[:date].strftime(I18n.t("rails_analytics.chart.date_format")),
87
+ formatted_count: ActiveSupport::NumberHelper.number_to_delimited(series[:count]))
88
+ end
89
+
90
+ def x(index)
36
91
  PAD + index * ((WIDTH - PAD) / [@series.length - 1, 1].max.to_f)
37
92
  end
38
93
 
39
- def dots(points_str)
40
- points_str.split(" ").map do |pt|
41
- x, y = pt.split(",")
42
- %(<circle cx="#{x}" cy="#{y}" r="3" fill="var(--ra-accent, #3b82f6)" />)
43
- end.join
94
+ def y(count, max)
95
+ HEIGHT - PAD - ((count / max) * (HEIGHT - PAD * 2))
96
+ end
97
+
98
+ # X-axis labels: first and last point always; month boundaries in between
99
+ # only when far enough from the neighbouring label to avoid collision.
100
+ def axis_labels
101
+ return "" if @series.length < 2
102
+
103
+ axis_indices.map do |i|
104
+ anchor = "start" if i.zero?
105
+ anchor = "end" if i == @series.length - 1
106
+ anchor ||= "middle"
107
+ %(<text x="#{x(i).round(2)}" y="#{HEIGHT + 13}" class="ra-axis-label" text-anchor="#{anchor}">#{write_date(@series[i][:date])}</text>)
108
+ end.join("\n")
109
+ end
110
+
111
+ def axis_indices
112
+ last = @series.length - 1
113
+ kept = [0]
114
+ (1...last).each do |i|
115
+ next unless @series[i][:date].day == 1 # month boundary
116
+
117
+ left_gap = x(i) - x(kept.last)
118
+ right_gap = x(last) - x(i)
119
+ kept << i if left_gap >= AXIS_MIN_GAP && right_gap >= AXIS_MIN_GAP
120
+ end
121
+ kept << last
122
+ kept.uniq
123
+ end
124
+
125
+ def write_date(date)
126
+ date.strftime(I18n.t("rails_analytics.chart.date_format"))
44
127
  end
45
128
 
46
129
  def empty_chart
@@ -51,4 +134,4 @@ module RailsAnalytics
51
134
  SVG
52
135
  end
53
136
  end
54
- end
137
+ end
@@ -14,6 +14,10 @@ en:
14
14
  chart:
15
15
  visits_per_day: "Visits per day"
16
16
  no_data: "No data"
17
+ date_format: "%m/%d"
18
+ tooltip:
19
+ one: "%{date} — 1 visit"
20
+ other: "%{date} — %{formatted_count} visits"
17
21
  tables:
18
22
  top_sources: "Traffic sources"
19
23
  top_events: "Top events"
@@ -14,6 +14,10 @@ pt-BR:
14
14
  chart:
15
15
  visits_per_day: "Visitas por dia"
16
16
  no_data: "Sem dados"
17
+ date_format: "%d/%m"
18
+ tooltip:
19
+ one: "%{date} — 1 visita"
20
+ other: "%{date} — %{formatted_count} visitas"
17
21
  tables:
18
22
  top_sources: "Origem do tráfego"
19
23
  top_events: "Clics por botão"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAnalytics
4
- VERSION = "1.0.4"
4
+ VERSION = "1.1.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_analytics
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - br4zz4
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-07 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails