sponsored_logs 0.1.0 → 0.2.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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be8c55d29f6cb9e44a75def20e54a4a7ca8b5c3fcb2eab2d973a67f1237847cc
|
|
4
|
+
data.tar.gz: '0033992014c5b8b54c847d7f7b139738323901721a965d718edb62b9820c3844'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdce138553b30eeb611c03013da3c80ce7e642927873b1ff7b07c91978cf72603f9f928b2f7c74c53cb5e553e64ced063dcfcbb4130c272bbdfcbb69a6ca034b
|
|
7
|
+
data.tar.gz: 0e71d8587e4d60f9ed8235fdf6f414a54a77a0b0ea337ed38b12bfc715bcf16698441c5ebc79be2e84f1066ded1330809d09953fdd86402d05e0fc091b422c2f
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module SponsoredLogs
|
|
4
4
|
module ReportsHelper
|
|
5
|
+
# Bright, saturated fills chosen for contrast against the badge's dark text,
|
|
6
|
+
# drawn from the banner palette (gold/cyan/green).
|
|
7
|
+
#
|
|
5
8
|
STATUS_COLORS = {
|
|
6
|
-
active: "#
|
|
7
|
-
scheduled: "#
|
|
9
|
+
active: "#10b981",
|
|
10
|
+
scheduled: "#38bdf8",
|
|
8
11
|
ended: "#6b7280",
|
|
9
|
-
evergreen: "#
|
|
12
|
+
evergreen: "#fbbf24",
|
|
13
|
+
exhausted: "#f59e0b"
|
|
10
14
|
}.freeze
|
|
11
15
|
|
|
12
16
|
BAR_HEIGHT = 22
|
|
@@ -14,7 +18,14 @@ module SponsoredLogs
|
|
|
14
18
|
LABEL_WIDTH = 320
|
|
15
19
|
TRACK_WIDTH = 360
|
|
16
20
|
VALUE_PAD = 8
|
|
17
|
-
|
|
21
|
+
|
|
22
|
+
# Segment palette for the share-of-spend donut, drawn from the banner
|
|
23
|
+
# (gold, cyan, greens, violets) and cycled for larger pools.
|
|
24
|
+
#
|
|
25
|
+
DONUT_COLORS = %w[
|
|
26
|
+
#fbbf24 #38bdf8 #10b981 #f59e0b #a78bfa
|
|
27
|
+
#34d399 #60a5fa #f472b6 #fb923c #22d3ee
|
|
28
|
+
].freeze
|
|
18
29
|
|
|
19
30
|
# Render a horizontal bar chart as inline SVG from report ad rows.
|
|
20
31
|
# `value` picks the numeric field per row; `format` renders the label.
|
|
@@ -47,6 +58,37 @@ module SponsoredLogs
|
|
|
47
58
|
)
|
|
48
59
|
end
|
|
49
60
|
|
|
61
|
+
# Share-of-spend donut as inline SVG. Each ad becomes an arc sized by its
|
|
62
|
+
# fraction of total spend, rendered as an offset stroke on a circle, with a
|
|
63
|
+
# legend beside it. Ads with zero spend are omitted.
|
|
64
|
+
#
|
|
65
|
+
def donut_chart(ads)
|
|
66
|
+
rows = ads.map { |ad| [ad[:text], ad[:spend].to_f] }
|
|
67
|
+
.select { |(_t, v)| v.positive? }
|
|
68
|
+
.sort_by { |(_t, v)| -v }
|
|
69
|
+
total = rows.sum { |(_t, v)| v }
|
|
70
|
+
return content_tag(:p, "No spend yet.", class: "empty") if total <= 0
|
|
71
|
+
|
|
72
|
+
radius = 60
|
|
73
|
+
donut_svg(donut_segments(rows, total, radius), donut_legend(rows, total), radius)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Delivery-to-goal bars for capped ads across all groups: a filled track
|
|
77
|
+
# showing impressions against the cap, so pacing is visible at a glance.
|
|
78
|
+
# Returns nil when no ad has a cap.
|
|
79
|
+
#
|
|
80
|
+
def cap_progress(report)
|
|
81
|
+
capped = %i[ads upcoming finished]
|
|
82
|
+
.flat_map { |group| report[group] || [] }
|
|
83
|
+
.select { |ad| ad[:cap] }
|
|
84
|
+
.uniq { |ad| ad[:text] }
|
|
85
|
+
.sort_by { |ad| -(ad[:impressions].to_f / ad[:cap]) }
|
|
86
|
+
return if capped.empty?
|
|
87
|
+
|
|
88
|
+
rows = capped.map { |ad| cap_progress_row(ad) }.join
|
|
89
|
+
content_tag(:div, raw(rows), class: "cap-list")
|
|
90
|
+
end
|
|
91
|
+
|
|
50
92
|
# Colored pill for an ad's flight status (:active/:scheduled/:ended/:evergreen).
|
|
51
93
|
#
|
|
52
94
|
def status_badge(status)
|
|
@@ -102,6 +144,80 @@ module SponsoredLogs
|
|
|
102
144
|
]))
|
|
103
145
|
end
|
|
104
146
|
|
|
147
|
+
def donut_segments(rows, total, radius)
|
|
148
|
+
circumference = 2 * Math::PI * radius
|
|
149
|
+
offset = 0.0
|
|
150
|
+
|
|
151
|
+
rows.each_with_index.map do |(_text, v), i|
|
|
152
|
+
frac = v / total
|
|
153
|
+
seg = donut_segment(frac, offset, radius, circumference, DONUT_COLORS[i % DONUT_COLORS.size])
|
|
154
|
+
offset += frac
|
|
155
|
+
seg
|
|
156
|
+
end.join
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def donut_legend(rows, total)
|
|
160
|
+
rows.each_with_index.map do |(text, v), i|
|
|
161
|
+
donut_legend_row(text, v, v / total, DONUT_COLORS[i % DONUT_COLORS.size])
|
|
162
|
+
end.join
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def donut_segment(frac, offset, radius, circumference, color)
|
|
166
|
+
dash = (frac * circumference).round(3)
|
|
167
|
+
gap = (circumference - dash).round(3)
|
|
168
|
+
# -offset rotates each segment to start where the previous ended;
|
|
169
|
+
# the whole ring is rotated -90deg (via the group) to begin at 12 o'clock.
|
|
170
|
+
#
|
|
171
|
+
dash_offset = (-offset * circumference).round(3)
|
|
172
|
+
|
|
173
|
+
%(<circle cx="80" cy="80" r="#{radius}" fill="none" stroke="#{color}"
|
|
174
|
+
stroke-width="26" stroke-dasharray="#{dash} #{gap}"
|
|
175
|
+
stroke-dashoffset="#{dash_offset}"/>)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def donut_legend_row(text, spend, frac, color)
|
|
179
|
+
pct = (frac * 100).round(1)
|
|
180
|
+
%(<div class="legend-row">
|
|
181
|
+
<span class="legend-swatch" style="background:#{color};"></span>
|
|
182
|
+
<span class="legend-label">#{esc(truncate_label(text))}</span>
|
|
183
|
+
<span class="legend-value">$#{format("%.2f", spend)} · #{pct}%</span>
|
|
184
|
+
</div>)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def donut_svg(segments, legend, radius)
|
|
188
|
+
hole = %(<circle cx="80" cy="80" r="#{radius - 20}" fill="#0f1727"/>)
|
|
189
|
+
ring = %(<g transform="rotate(-90 80 80)">#{segments}</g>)
|
|
190
|
+
svg = content_tag(
|
|
191
|
+
:svg,
|
|
192
|
+
raw(ring + hole),
|
|
193
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
194
|
+
viewBox: "0 0 160 160",
|
|
195
|
+
role: "img",
|
|
196
|
+
class: "donut",
|
|
197
|
+
style: "flex:0 0 160px;width:160px;height:160px;"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
content_tag(:div, safe_join([svg, content_tag(:div, raw(legend), class: "legend")]),
|
|
201
|
+
class: "donut-wrap")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def cap_progress_row(ad)
|
|
205
|
+
cap = ad[:cap].to_i
|
|
206
|
+
imp = ad[:impressions].to_i
|
|
207
|
+
pct = cap.positive? ? [(imp.to_f / cap * 100), 100].min.round(1) : 0.0
|
|
208
|
+
full = pct >= 100
|
|
209
|
+
|
|
210
|
+
%(<div class="cap-row">
|
|
211
|
+
<div class="cap-head">
|
|
212
|
+
<span class="cap-label">#{esc(truncate_label(ad[:text]))}</span>
|
|
213
|
+
<span class="cap-count">#{imp} / #{cap}#{" ✓" if full}</span>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="cap-track">
|
|
216
|
+
<div class="cap-fill#{" full" if full}" style="width:#{pct}%;"></div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>)
|
|
219
|
+
end
|
|
220
|
+
|
|
105
221
|
def svg_bar(label, value_label, y, bar_w)
|
|
106
222
|
text_y = y + (BAR_HEIGHT / 2) + 4
|
|
107
223
|
label_text = esc(truncate_label(label))
|
|
@@ -4,43 +4,150 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>Campaign Performance</title>
|
|
6
6
|
<style>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
:root {
|
|
8
|
+
--bg-0: #0b0f19;
|
|
9
|
+
--bg-1: #161e2e;
|
|
10
|
+
--panel: #111827;
|
|
11
|
+
--panel-2: #1f2937;
|
|
12
|
+
--border: #374151;
|
|
13
|
+
--border-soft: #243044;
|
|
14
|
+
--text: #e5e7eb;
|
|
15
|
+
--muted: #9ca3af;
|
|
16
|
+
--faint: #6b7280;
|
|
17
|
+
--gold-0: #f59e0b;
|
|
18
|
+
--gold-1: #fbbf24;
|
|
19
|
+
--cyan: #38bdf8;
|
|
20
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
21
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
22
|
+
}
|
|
23
|
+
* { box-sizing: border-box; }
|
|
24
|
+
body {
|
|
25
|
+
font-family: var(--sans);
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 2.5rem 1.5rem;
|
|
28
|
+
color: var(--text);
|
|
29
|
+
background: linear-gradient(135deg, var(--bg-0), var(--bg-1));
|
|
30
|
+
background-attachment: fixed;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
}
|
|
33
|
+
.wrap { max-width: 64rem; margin: 0 auto; }
|
|
34
|
+
|
|
35
|
+
.masthead {
|
|
36
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
37
|
+
border: 1px solid var(--border); border-radius: 14px;
|
|
38
|
+
background: linear-gradient(180deg, var(--panel-2), var(--panel));
|
|
39
|
+
padding: 1.1rem 1.4rem; margin-bottom: 1.75rem;
|
|
40
|
+
}
|
|
41
|
+
.masthead .dots { display: flex; gap: 6px; margin-right: 0.4rem; }
|
|
42
|
+
.masthead .dots i { width: 10px; height: 10px; border-radius: 50%; display: block; }
|
|
43
|
+
.masthead .dots .r { background: #ef4444; }
|
|
44
|
+
.masthead .dots .y { background: var(--gold-0); }
|
|
45
|
+
.masthead .dots .g { background: #10b981; }
|
|
46
|
+
.masthead h1 { font-size: 1.25rem; margin: 0; font-weight: 800; letter-spacing: -0.02em; }
|
|
47
|
+
.masthead h1 .gold { color: var(--gold-1); }
|
|
48
|
+
.masthead .subtitle { color: var(--cyan); margin: 0.15rem 0 0; font-size: 0.72rem;
|
|
49
|
+
font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
50
|
+
.masthead .promoted { margin-left: auto; font-family: var(--mono); font-size: 0.62rem;
|
|
51
|
+
font-weight: 700; letter-spacing: 0.1em; color: var(--gold-0);
|
|
52
|
+
border: 1px solid var(--border); border-radius: 5px; padding: 0.3rem 0.55rem;
|
|
53
|
+
background: #0f1625; }
|
|
54
|
+
|
|
55
|
+
.totals { display: flex; flex-wrap: wrap; gap: 1rem; margin: 0 0 2rem; }
|
|
56
|
+
.totals .card {
|
|
57
|
+
flex: 1 1 12rem;
|
|
58
|
+
background: linear-gradient(180deg, var(--panel-2), var(--panel));
|
|
59
|
+
border: 1px solid var(--border); border-radius: 12px; padding: 1.1rem 1.3rem;
|
|
60
|
+
}
|
|
61
|
+
.totals .label { display: block; font-size: 0.66rem; text-transform: uppercase;
|
|
62
|
+
letter-spacing: 0.09em; color: var(--muted); font-family: var(--mono); }
|
|
63
|
+
.totals .value { font-size: 1.9rem; font-weight: 800; font-family: var(--mono);
|
|
64
|
+
color: var(--gold-1); margin-top: 0.35rem; }
|
|
65
|
+
|
|
66
|
+
h2 { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.09em;
|
|
67
|
+
color: var(--cyan); font-family: var(--mono); margin: 2.25rem 0 0.75rem; }
|
|
68
|
+
|
|
69
|
+
table { width: 100%; border-collapse: collapse; margin-top: 0.5rem;
|
|
70
|
+
border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
|
|
71
|
+
background: var(--panel); }
|
|
72
|
+
th, td { text-align: left; padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--border-soft); }
|
|
73
|
+
tr:last-child td { border-bottom: none; }
|
|
74
|
+
th { font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.08em;
|
|
75
|
+
color: var(--muted); font-family: var(--mono); background: var(--panel-2); }
|
|
76
|
+
td { font-size: 0.85rem; }
|
|
77
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); }
|
|
78
|
+
tbody tr:hover { background: #0f1727; }
|
|
79
|
+
|
|
80
|
+
.empty { color: var(--faint); font-style: italic; margin-top: 1.5rem;
|
|
81
|
+
font-family: var(--mono); }
|
|
82
|
+
|
|
23
83
|
.chart { display: block; }
|
|
24
|
-
.chart .bar-label { font-size: 12px; fill:
|
|
25
|
-
.chart .bar-value { font-size: 12px; fill:
|
|
26
|
-
|
|
27
|
-
.chart .bar-
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
84
|
+
.chart .bar-label { font-size: 12px; fill: var(--muted); font-family: var(--mono); }
|
|
85
|
+
.chart .bar-value { font-size: 12px; fill: var(--gold-1); font-variant-numeric: tabular-nums;
|
|
86
|
+
font-family: var(--mono); }
|
|
87
|
+
.chart .bar-track { fill: #0f1727; rx: 4; }
|
|
88
|
+
.chart .bar-fill { fill: url(#slGold); rx: 4; }
|
|
89
|
+
|
|
90
|
+
.badge { display: inline-block; padding: 0.15rem 0.55rem; border-radius: 999px;
|
|
91
|
+
color: #0b0f19; font-size: 0.62rem; text-transform: uppercase;
|
|
92
|
+
letter-spacing: 0.04em; font-weight: 800; font-family: var(--mono); }
|
|
93
|
+
td.flight { font-variant-numeric: tabular-nums; color: var(--muted);
|
|
94
|
+
white-space: nowrap; font-family: var(--mono); font-size: 0.8rem; }
|
|
95
|
+
|
|
96
|
+
/* Share-of-spend donut */
|
|
97
|
+
.donut-wrap { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;
|
|
98
|
+
border: 1px solid var(--border); border-radius: 12px;
|
|
99
|
+
background: var(--panel); padding: 1.2rem 1.4rem; }
|
|
100
|
+
.legend { flex: 1 1 16rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
101
|
+
.legend-row { display: flex; align-items: center; gap: 0.6rem; font-size: 0.82rem; }
|
|
102
|
+
.legend-swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 12px; }
|
|
103
|
+
.legend-label { color: var(--text); overflow: hidden; text-overflow: ellipsis;
|
|
104
|
+
white-space: nowrap; }
|
|
105
|
+
.legend-value { margin-left: auto; color: var(--muted); font-family: var(--mono);
|
|
106
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
107
|
+
|
|
108
|
+
/* Delivery-to-goal (cap) progress */
|
|
109
|
+
.cap-list { display: flex; flex-direction: column; gap: 0.9rem;
|
|
110
|
+
border: 1px solid var(--border); border-radius: 12px;
|
|
111
|
+
background: var(--panel); padding: 1.2rem 1.4rem; }
|
|
112
|
+
.cap-head { display: flex; justify-content: space-between; align-items: baseline;
|
|
113
|
+
margin-bottom: 0.35rem; font-size: 0.82rem; }
|
|
114
|
+
.cap-label { color: var(--text); overflow: hidden; text-overflow: ellipsis;
|
|
115
|
+
white-space: nowrap; margin-right: 1rem; }
|
|
116
|
+
.cap-count { color: var(--muted); font-family: var(--mono);
|
|
117
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
118
|
+
.cap-track { height: 10px; border-radius: 999px; background: #0f1727; overflow: hidden; }
|
|
119
|
+
.cap-fill { height: 100%; border-radius: 999px;
|
|
120
|
+
background: linear-gradient(90deg, var(--gold-0), var(--gold-1)); }
|
|
121
|
+
.cap-fill.full { background: linear-gradient(90deg, #10b981, #34d399); }
|
|
32
122
|
</style>
|
|
33
123
|
</head>
|
|
34
124
|
<body>
|
|
35
|
-
|
|
36
|
-
<
|
|
125
|
+
<!-- Shared gradient for SVG bar fills, matching the banner's gold. -->
|
|
126
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
127
|
+
<defs>
|
|
128
|
+
<linearGradient id="slGold" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
129
|
+
<stop offset="0%" stop-color="#f59e0b" />
|
|
130
|
+
<stop offset="100%" stop-color="#fbbf24" />
|
|
131
|
+
</linearGradient>
|
|
132
|
+
</defs>
|
|
133
|
+
</svg>
|
|
134
|
+
|
|
135
|
+
<div class="wrap">
|
|
136
|
+
<header class="masthead">
|
|
137
|
+
<span class="dots"><i class="r"></i><i class="y"></i><i class="g"></i></span>
|
|
138
|
+
<div>
|
|
139
|
+
<h1>Sponsored<span class="gold">Logs</span> <span style="color:var(--muted);font-weight:600;">Command Center</span></h1>
|
|
140
|
+
<p class="subtitle">Campaign performance · live delivery report</p>
|
|
141
|
+
</div>
|
|
142
|
+
<span class="promoted">SPONSORED</span>
|
|
143
|
+
</header>
|
|
37
144
|
|
|
38
145
|
<div class="totals">
|
|
39
|
-
<div>
|
|
146
|
+
<div class="card">
|
|
40
147
|
<span class="label">Impressions</span>
|
|
41
148
|
<span class="value"><%= @report[:impressions] %></span>
|
|
42
149
|
</div>
|
|
43
|
-
<div>
|
|
150
|
+
<div class="card">
|
|
44
151
|
<span class="label">Total spend</span>
|
|
45
152
|
<span class="value">$<%= format("%.2f", @report[:spend]) %></span>
|
|
46
153
|
</div>
|
|
@@ -49,6 +156,9 @@
|
|
|
49
156
|
<% if @report[:ads].empty? %>
|
|
50
157
|
<p class="empty">No impressions delivered yet.</p>
|
|
51
158
|
<% else %>
|
|
159
|
+
<h2>Share of spend</h2>
|
|
160
|
+
<%= donut_chart(@report[:ads]) %>
|
|
161
|
+
|
|
52
162
|
<h2>Spend by advertiser</h2>
|
|
53
163
|
<%= bar_chart(@report[:ads], value: ->(ad) { ad[:spend] }, format: ->(v) { "$#{format('%.2f', v)}" }) %>
|
|
54
164
|
|
|
@@ -59,6 +169,12 @@
|
|
|
59
169
|
<%= campaign_table(@report[:ads]) %>
|
|
60
170
|
<% end %>
|
|
61
171
|
|
|
172
|
+
<% cap = cap_progress(@report) %>
|
|
173
|
+
<% if cap %>
|
|
174
|
+
<h2>Delivery to goal</h2>
|
|
175
|
+
<%= cap %>
|
|
176
|
+
<% end %>
|
|
177
|
+
|
|
62
178
|
<% if @report[:upcoming].any? %>
|
|
63
179
|
<h2>Upcoming campaigns</h2>
|
|
64
180
|
<%= campaign_table(@report[:upcoming]) %>
|
|
@@ -68,5 +184,6 @@
|
|
|
68
184
|
<h2>Finished campaigns</h2>
|
|
69
185
|
<%= campaign_table(@report[:finished]) %>
|
|
70
186
|
<% end %>
|
|
187
|
+
</div>
|
|
71
188
|
</body>
|
|
72
189
|
</html>
|