feat: 热词热力图改为Treemap矩形堆砌布局,面积代表提及次数;赛道分析页面布局调整

This commit is contained in:
freedakgmail
2026-07-21 07:59:25 +08:00
parent a40b0f14ae
commit 3c4916ba04
8 changed files with 284 additions and 81 deletions
+87 -27
View File
@@ -45,9 +45,11 @@ def now_iso() -> str:
def db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn = sqlite3.connect(DB_PATH, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA busy_timeout = 5000")
return conn
@@ -160,7 +162,7 @@ def dashboard():
"type": r["type"] or "经营干货",
"score": r["score"] or 0,
"summary": r["ai_summary"] or "",
"entities": json.loads(r["entities"]) if r["entities"] else [],
"entities": [b["name"] if isinstance(b, dict) else b for b in (json.loads(r["entities"]) if r["entities"] else [])],
}
for r in top_intel
],
@@ -251,7 +253,7 @@ def intel_list():
"summary": r["ai_summary"] or r["summary"] or "",
"keyPoints": json.loads(r["key_points"]) if r["key_points"] else [],
"score": r["score"] or 0,
"entities": json.loads(r["entities"]) if r["entities"] else [],
"entities": [b["name"] if isinstance(b, dict) else b for b in (json.loads(r["entities"]) if r["entities"] else [])],
"timeliness": r["timeliness"] or "",
"tags": json.loads(r["tags"]) if r["tags"] else [],
"url": r["canonical_url"],
@@ -393,26 +395,41 @@ def brand_detail(brand_id: int):
@app.get("/api/analysis")
def analysis():
with db() as conn:
# 赛道热度趋势:按月统计各赛道的平均评分
# 赛道热度趋势:按月统计各赛道的文章数和平均评分,热度=文章数×平均评分/10
heat_rows = conn.execute(
"""
SELECT strftime('%m', a.published_at) AS month,
SELECT strftime('%Y-%m', a.published_at) AS month,
a.category,
AVG(CAST(json_extract(x.result_json, '$.score') AS REAL)) AS heat
COUNT(*) AS article_cnt,
AVG(CAST(json_extract(x.result_json, '$.score') AS REAL)) AS avg_score
FROM articles a
LEFT JOIN ai_analyses x ON x.article_id = a.id AND x.analysis_type = 'editorial'
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL AND a.category != ''
AND a.published_at >= date('now', '-12 months')
GROUP BY month, a.category
ORDER BY month
"""
).fetchall()
# 开关店对比(从文章中提取的品类趋势数据
category_counts = conn.execute(
# 品牌门店规模(按赛道聚合真实门店总数
brand_stores = conn.execute(
"""
SELECT a.category, COUNT(*) AS cnt
SELECT category, SUM(stores) AS total_stores, COUNT(*) AS brand_cnt
FROM brands WHERE category IS NOT NULL AND category != '' AND stores > 0
GROUP BY category
"""
).fetchall()
# 舆情分布(按赛道统计正面/负面/中性文章数)
sentiment_rows = conn.execute(
"""
SELECT a.category,
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'positive' THEN 1 ELSE 0 END) AS positive_cnt,
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'negative' THEN 1 ELSE 0 END) AS negative_cnt,
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'neutral' OR json_extract(x.result_json, '$.sentiment') IS NULL THEN 1 ELSE 0 END) AS neutral_cnt
FROM articles a
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL
JOIN ai_analyses x ON x.article_id = a.id AND x.analysis_type = 'editorial'
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL AND a.category != ''
GROUP BY a.category
"""
).fetchall()
@@ -432,29 +449,55 @@ def analysis():
"""
).fetchone()
# 热词
tag_rows = conn.execute(
# 热词热力图:近7天每天各标签出现次数
tag_heatmap = conn.execute(
"""
SELECT value AS tag, COUNT(*) AS cnt
FROM ai_analyses, json_each(json_extract(result_json, '$.tags'))
GROUP BY value
ORDER BY cnt DESC
LIMIT 8
SELECT value AS tag,
strftime('%Y-%m-%d', a.published_at) AS day,
COUNT(*) AS cnt
FROM ai_analyses x
JOIN articles a ON a.id = x.article_id
, json_each(json_extract(x.result_json, '$.tags'))
WHERE a.published_at >= date('now', '-6 days')
AND value IS NOT NULL AND value != ''
GROUP BY tag, day
"""
).fetchall()
# 构建热度趋势数据
# 热词总量排行(用于选取 TOP 标签)
tag_total = conn.execute(
"""
SELECT value AS tag, COUNT(*) AS cnt
FROM ai_analyses x
JOIN articles a ON a.id = x.article_id
, json_each(json_extract(x.result_json, '$.tags'))
WHERE a.published_at >= date('now', '-6 days')
AND value IS NOT NULL AND value != ''
GROUP BY value
ORDER BY cnt DESC
LIMIT 10
"""
).fetchall()
# 构建热度趋势数据(热度 = 文章数 × 平均评分 / 10)
heat_trend = {}
for r in heat_rows:
month_key = f"{r['month']}" if r["month"] else "未知"
month_key = f"{r['month'][-2:]}" if r["month"] else "未知"
if month_key not in heat_trend:
heat_trend[month_key] = {"month": month_key}
heat_trend[month_key][r["category"]] = round(r["heat"] or 0)
heat_val = round((r["article_cnt"] or 0) * (r["avg_score"] or 0) / 10)
heat_trend[month_key][r["category"]] = heat_val
# 构建开关店数据
open_close = [
{"category": r["category"], "新开": r["cnt"], "关闭": max(0, r["cnt"] - int(r["cnt"] * 0.85))}
for r in category_counts
# 构建品牌门店规模数据
brand_stores_data = [
{"category": r["category"], "stores": r["total_stores"] or 0, "brands": r["brand_cnt"]}
for r in brand_stores
]
# 构建舆情分布数据
sentiment_data = [
{"category": r["category"], "正面": r["positive_cnt"] or 0, "负面": r["negative_cnt"] or 0, "中性": r["neutral_cnt"] or 0}
for r in sentiment_rows
]
# 构建价格分布
@@ -468,11 +511,28 @@ def analysis():
{"band": "150元以上", "pct": round((price_rows["band6"] or 0) * 100 / total_brands)},
]
# 构建热词热力图数据(用 UTC 日期与 SQLite date('now') 保持一致)
from datetime import timedelta, timezone
today_utc = datetime.now(timezone.utc).date()
days = [(today_utc - timedelta(days=i)).isoformat() for i in range(6, -1, -1)]
top_tags = [r["tag"] for r in tag_total]
# 构建矩阵 {tag: {day: cnt}}
matrix = {tag: {day: 0 for day in days} for tag in top_tags}
for r in tag_heatmap:
if r["tag"] in matrix and r["day"] in matrix[r["tag"]]:
matrix[r["tag"]][r["day"]] = r["cnt"]
hot_keywords = {
"days": days,
"tags": top_tags,
"matrix": matrix,
}
return jsonify({
"categoryHeatTrend": list(heat_trend.values()),
"openCloseByCategory": open_close,
"brandStoresByCategory": brand_stores_data,
"sentimentByCategory": sentiment_data,
"priceBandData": price_bands,
"hotKeywords": [{"word": r["tag"], "change": min(r["cnt"] * 5, 50)} for r in tag_rows],
"hotKeywords": hot_keywords,
})