feat(Метрики МП): daily scrollable charts, MAU max/month, DAU avg, iOS+Android lines

- updater: monthly MAU = MAX over month, monthly DAU = AVG; add monthly
  iOS/Android series + daily series (last 540 days) to top{} JSON
- modal: monthly chart + scrollable daily chart (scrolls to latest); downloads
  show Всего + semi-transparent iOS/Android lines (both monthly & daily)
This commit is contained in:
Iliyas 2026-06-17 14:07:48 +05:00
parent 60abc04ea5
commit 5b78a24439
2 changed files with 3963 additions and 67 deletions

File diff suppressed because it is too large Load Diff

View File

@ -164,7 +164,8 @@ from t
left join t1 on t.entry_date = t1.entry_date
""".strip()
TOP_TREND_MONTHS = 13 # сколько последних месяцев показывать в тренде верхних карточек
TOP_TREND_MONTHS = 13 # сколько последних месяцев показывать в месячном тренде
TOP_DAILY_DAYS = 540 # сколько последних дней отдавать в дневной график (со скроллом)
def fetch_top(conn):
@ -202,47 +203,64 @@ def build_top(recs):
return None
last = recs[-1]
def _i(v):
return int(v) if v is not None else 0
def last_nonnull(field):
for r in reversed(recs):
if r[field] is not None:
return int(r[field]), r["date"]
return 0, last["date"]
return int(r[field])
return 0
mau_v, _ = last_nonnull("mau")
dau_v, _ = last_nonnull("dau")
# помесячная агрегация: month-end для кумулятивных, среднее для активности
# помесячная агрегация:
# registered/installs — month-end (последнее значение нарастающего итога в месяце)
# MAU — МАКСИМУМ за месяц; DAU — СРЕДНЕЕ за месяц
months = {} # rpid -> aggregate
for r in recs:
m = months.setdefault(r["rpid"], {
"last_date": "", "registered_total": 0, "sum_installs": 0,
"mau_sum": 0, "mau_cnt": 0, "dau_sum": 0, "dau_cnt": 0,
"last_date": "", "registered_total": 0, "sum_installs": 0, "ios": 0, "android": 0,
"mau_max": 0, "dau_sum": 0, "dau_cnt": 0,
})
if r["date"] >= m["last_date"]:
m["last_date"] = r["date"]
if r["registered_total"] is not None:
m["registered_total"] = int(r["registered_total"])
if r["sum_installs"] is not None:
m["sum_installs"] = int(r["sum_installs"])
m["registered_total"] = _i(r["registered_total"])
m["sum_installs"] = _i(r["sum_installs"])
m["ios"] = _i(r["ios_installs"])
m["android"] = _i(r["android_installs"])
if r["mau"] is not None:
m["mau_sum"] += int(r["mau"]); m["mau_cnt"] += 1
m["mau_max"] = max(m["mau_max"], int(r["mau"]))
if r["dau"] is not None:
m["dau_sum"] += int(r["dau"]); m["dau_cnt"] += 1
ordered = sorted(m for m in months if m is not None)
ordered = ordered[-TOP_TREND_MONTHS:]
labels, reg_s, inst_s, mau_s, dau_s = [], [], [], [], []
ordered = sorted(months)[-TOP_TREND_MONTHS:]
labels, reg_s, inst_s, ios_s, andr_s, mau_s, dau_s = [], [], [], [], [], [], []
for rpid in ordered:
a = months[rpid]
y, mo = rpid // 100, rpid % 100
labels.append(f"{_MONTHS_RU_SHORT[mo]} {y % 100:02d}")
reg_s.append(a["registered_total"])
inst_s.append(a["sum_installs"])
mau_s.append(round(a["mau_sum"] / a["mau_cnt"]) if a["mau_cnt"] else 0)
ios_s.append(a["ios"])
andr_s.append(a["android"])
mau_s.append(a["mau_max"])
dau_s.append(round(a["dau_sum"] / a["dau_cnt"]) if a["dau_cnt"] else 0)
def _i(v):
return int(v) if v is not None else 0
# MAU в карточке — максимум за текущий (последний) месяц; DAU — последнее дневное значение
cur_month_max_mau = months[last["rpid"]]["mau_max"]
mau_headline = cur_month_max_mau if cur_month_max_mau else last_nonnull("mau")
dau_headline = last_nonnull("dau")
# дневные ряды (последние N дней) для графика со скроллом
daily_recs = recs[-TOP_DAILY_DAYS:]
daily = {
"labels": [r["date"] for r in daily_recs],
"registered": [_i(r["registered_total"]) for r in daily_recs],
"installs": [_i(r["sum_installs"]) for r in daily_recs],
"installs_ios": [_i(r["ios_installs"]) for r in daily_recs],
"installs_android": [_i(r["android_installs"]) for r in daily_recs],
"mau": [int(r["mau"]) if r["mau"] is not None else None for r in daily_recs],
"dau": [int(r["dau"]) if r["dau"] is not None else None for r in daily_recs],
}
return {
"as_of": last["date"],
@ -250,13 +268,16 @@ def build_top(recs):
"installs_total": _i(last["sum_installs"]),
"installs_ios": _i(last["ios_installs"]),
"installs_android": _i(last["android_installs"]),
"mau": mau_v,
"dau": dau_v,
"mau": mau_headline,
"dau": dau_headline,
"trend_labels": labels,
"registered_series": reg_s,
"installs_series": inst_s,
"installs_ios_series": ios_s,
"installs_android_series": andr_s,
"mau_series": mau_s,
"dau_series": dau_s,
"daily": daily,
}