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 +4 -4
- data/README.md +4 -0
- data/app/assets/stylesheets/rails_analytics/dashboard.css +27 -0
- data/app/models/rails_analytics/chart_builder.rb +98 -15
- data/config/locales/rails_analytics.en.yml +4 -0
- data/config/locales/rails_analytics.pt-BR.yml +4 -0
- data/lib/rails_analytics/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6a607ecb6f634b056bce7db997068d29e41ecc29c20d1fe38fb352a2e81b70d
|
|
4
|
+
data.tar.gz: 9ce65316e5f58c2c644df424315d29597d129560ead62e530c2499a1cbd2c26f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
#{
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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"
|
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.
|
|
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-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|