fix(douyu): aggregate audience trend by minute
This commit is contained in:
@@ -891,18 +891,18 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
|
||||
@staticmethod
|
||||
def _normalize_audience_points(points: List[Dict[str, Any]], limit: int = 720) -> List[Dict[str, Any]]:
|
||||
normalized: List[Dict[str, Any]] = []
|
||||
seen = set()
|
||||
normalized_map: Dict[str, Dict[str, Any]] = {}
|
||||
for item in points or []:
|
||||
timestamp = str(item.get("timestamp") or "").strip()
|
||||
if not timestamp or timestamp in seen:
|
||||
if not timestamp:
|
||||
continue
|
||||
seen.add(timestamp)
|
||||
normalized.append({
|
||||
minute_key = timestamp[:16]
|
||||
normalized_map[minute_key] = {
|
||||
"timestamp": timestamp,
|
||||
"vip_count": int(item.get("vip_count", 0) or 0),
|
||||
"diamond_count": int(item.get("diamond_count", 0) or 0),
|
||||
})
|
||||
}
|
||||
normalized = list(normalized_map.values())
|
||||
normalized.sort(key=lambda row: row.get("timestamp", ""))
|
||||
if len(normalized) > limit:
|
||||
normalized = normalized[-limit:]
|
||||
@@ -1183,7 +1183,16 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
|
||||
def _build_audience_trend(self, sessions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
points: List[Dict[str, Any]] = []
|
||||
segment_start_times: List[str] = []
|
||||
segment_end_times: List[str] = []
|
||||
for session in sessions:
|
||||
for segment in session.get("segments", []) or []:
|
||||
start_time = str(segment.get("start_time") or "").strip()
|
||||
end_time = str(segment.get("end_time") or "").strip()
|
||||
if start_time:
|
||||
segment_start_times.append(start_time)
|
||||
if end_time:
|
||||
segment_end_times.append(end_time)
|
||||
for item in session.get("audience_points", []) or []:
|
||||
point = {
|
||||
"timestamp": str(item.get("timestamp") or "").strip(),
|
||||
@@ -1198,6 +1207,16 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
vip_values = [int(item.get("vip_count", 0) or 0) for item in points]
|
||||
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points]
|
||||
labels = [str(item.get("timestamp") or "")[-8:-3] for item in points]
|
||||
session_start = min(segment_start_times) if segment_start_times else ""
|
||||
session_end = max(segment_end_times) if segment_end_times else str(points[-1].get("timestamp") or "")
|
||||
first_point_time = str(points[0].get("timestamp") or "")
|
||||
last_point_time = str(points[-1].get("timestamp") or "")
|
||||
leading_gap_minutes = 0
|
||||
if session_start and first_point_time:
|
||||
start_dt = self._parse_session_time(session_start)
|
||||
point_dt = self._parse_session_time(first_point_time)
|
||||
if start_dt and point_dt:
|
||||
leading_gap_minutes = max(int((point_dt - start_dt).total_seconds() // 60), 0)
|
||||
return {
|
||||
"points": points,
|
||||
"summary": {
|
||||
@@ -1209,6 +1228,11 @@ class DouyuPlugin(MessagePluginInterface):
|
||||
"diamond_max": max(diamond_values),
|
||||
"diamond_latest": diamond_values[-1],
|
||||
"labels": labels,
|
||||
"session_start": session_start,
|
||||
"session_end": session_end,
|
||||
"first_point_time": first_point_time,
|
||||
"last_point_time": last_point_time,
|
||||
"leading_gap_minutes": leading_gap_minutes,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -209,28 +209,56 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
|
||||
plot_width = width - padding_left - padding_right
|
||||
plot_height = height - padding_top - padding_bottom
|
||||
|
||||
vip_values = [int(item.get("vip_count", 0) or 0) for item in points]
|
||||
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in points]
|
||||
labels = [str(item.get("timestamp") or "")[-8:-3] for item in points]
|
||||
summary = audience_trend.get("summary", {}) or {}
|
||||
|
||||
def _compress_chart_points(raw_points: List[Dict[str, Any]], max_points: int = 36) -> List[Dict[str, Any]]:
|
||||
if len(raw_points) <= max_points:
|
||||
return list(raw_points)
|
||||
bucket_size = max(len(raw_points) / max_points, 1)
|
||||
compressed: List[Dict[str, Any]] = []
|
||||
for idx in range(max_points):
|
||||
start = int(round(idx * bucket_size))
|
||||
end = int(round((idx + 1) * bucket_size))
|
||||
bucket = raw_points[start:end] or raw_points[start:start + 1]
|
||||
if not bucket:
|
||||
continue
|
||||
compressed.append(dict(bucket[-1]))
|
||||
first = raw_points[0]
|
||||
last = raw_points[-1]
|
||||
if compressed:
|
||||
compressed[0] = dict(first)
|
||||
compressed[-1] = dict(last)
|
||||
return compressed
|
||||
|
||||
chart_points = _compress_chart_points(points, max_points=36)
|
||||
vip_values = [int(item.get("vip_count", 0) or 0) for item in chart_points]
|
||||
diamond_values = [int(item.get("diamond_count", 0) or 0) for item in chart_points]
|
||||
labels = [str(item.get("timestamp") or "")[-8:-3] for item in chart_points]
|
||||
|
||||
vip_min = min(vip_values)
|
||||
vip_max = max(vip_values)
|
||||
diamond_min = min(diamond_values)
|
||||
diamond_max = max(diamond_values)
|
||||
vip_span = max(vip_max - vip_min, 1)
|
||||
diamond_span = max(diamond_max - diamond_min, 1)
|
||||
step_x = plot_width / max(len(points) - 1, 1)
|
||||
vip_padding = max(int((vip_max - vip_min) * 0.12), 60)
|
||||
diamond_padding = 1 if diamond_max - diamond_min <= 2 else max(int((diamond_max - diamond_min) * 0.2), 1)
|
||||
vip_display_min = max(vip_min - vip_padding, 0)
|
||||
vip_display_max = vip_max + vip_padding
|
||||
diamond_display_min = max(diamond_min - diamond_padding, 0)
|
||||
diamond_display_max = diamond_max + diamond_padding
|
||||
vip_span = max(vip_display_max - vip_display_min, 1)
|
||||
diamond_span = max(diamond_display_max - diamond_display_min, 1)
|
||||
step_x = plot_width / max(len(chart_points) - 1, 1)
|
||||
bar_width = max(min(step_x * 0.58, 18), 6)
|
||||
|
||||
def x_at(index: int) -> float:
|
||||
return padding_left + step_x * index
|
||||
|
||||
def y_vip(value: int) -> float:
|
||||
ratio = (value - vip_min) / vip_span if vip_span else 0
|
||||
ratio = (value - vip_display_min) / vip_span if vip_span else 0
|
||||
return padding_top + plot_height - ratio * plot_height
|
||||
|
||||
def y_diamond(value: int) -> float:
|
||||
ratio = (value - diamond_min) / diamond_span if diamond_span else 0
|
||||
ratio = (value - diamond_display_min) / diamond_span if diamond_span else 0
|
||||
return padding_top + plot_height - ratio * plot_height
|
||||
|
||||
grid_lines = []
|
||||
@@ -239,8 +267,8 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
|
||||
for idx in range(5):
|
||||
ratio = idx / 4
|
||||
y = padding_top + plot_height - ratio * plot_height
|
||||
vip_tick = round(vip_min + vip_span * ratio)
|
||||
diamond_tick = round(diamond_min + diamond_span * ratio)
|
||||
vip_tick = round(vip_display_min + vip_span * ratio)
|
||||
diamond_tick = round(diamond_display_min + diamond_span * ratio)
|
||||
grid_lines.append(
|
||||
f'<line x1="{padding_left}" y1="{y:.1f}" x2="{width - padding_right}" y2="{y:.1f}" class="chart-grid" />'
|
||||
)
|
||||
@@ -256,14 +284,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
|
||||
dot_points = []
|
||||
label_marks = []
|
||||
annotations = []
|
||||
if len(points) <= 12:
|
||||
label_indexes = list(range(len(points)))
|
||||
if len(chart_points) <= 12:
|
||||
label_indexes = list(range(len(chart_points)))
|
||||
else:
|
||||
label_indexes = sorted(set([0, len(points) - 1] + [int(round((len(points) - 1) * i / 6)) for i in range(1, 6)]))
|
||||
label_indexes = sorted(set([0, len(chart_points) - 1] + [int(round((len(chart_points) - 1) * i / 6)) for i in range(1, 6)]))
|
||||
vip_peak_index = vip_values.index(vip_max)
|
||||
vip_valley_index = vip_values.index(vip_min)
|
||||
diamond_latest_index = len(points) - 1
|
||||
for idx, item in enumerate(points):
|
||||
diamond_latest_index = len(chart_points) - 1
|
||||
for idx, item in enumerate(chart_points):
|
||||
x = x_at(idx)
|
||||
vip_y = y_vip(int(item.get("vip_count", 0) or 0))
|
||||
diamond_y = y_diamond(int(item.get("diamond_count", 0) or 0))
|
||||
@@ -300,9 +328,14 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
|
||||
)
|
||||
|
||||
polyline = f'<polyline points="{" ".join(line_points)}" class="diamond-line" />'
|
||||
summary = audience_trend.get("summary", {}) or {}
|
||||
vip_latest = int(summary.get("vip_latest", vip_values[-1]) or 0)
|
||||
diamond_latest = int(summary.get("diamond_latest", diamond_values[-1]) or 0)
|
||||
session_start = str(summary.get("session_start") or "")
|
||||
first_point_time = str(summary.get("first_point_time") or "")
|
||||
leading_gap_minutes = int(summary.get("leading_gap_minutes", 0) or 0)
|
||||
note_text = ""
|
||||
if session_start and first_point_time and leading_gap_minutes >= 20:
|
||||
note_text = f"采样起点 {first_point_time[-8:-3]},早于该时段的人数数据未记录"
|
||||
|
||||
return f"""
|
||||
<div class="chart-wrap">
|
||||
@@ -314,6 +347,7 @@ def _render_audience_trend_chart(audience_trend: Dict[str, Any]) -> str:
|
||||
<span class="legend-meta">最新:贵宾 {_escape(vip_latest)} / 钻粉 {_escape(diamond_latest)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{'<div class="chart-note">' + _escape(note_text) + '</div>' if note_text else ''}
|
||||
<svg viewBox="0 0 {width} {height}" class="audience-chart" role="img" aria-label="贵宾与钻粉趋势图">
|
||||
<defs>
|
||||
<linearGradient id="vipBarGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
@@ -744,6 +778,12 @@ def render_daily_report_html(
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.chart-note {{
|
||||
margin: -2px 0 8px;
|
||||
color: #7b8798;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}}
|
||||
.chart-title {{
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
|
||||
Reference in New Issue
Block a user