前面我们搞定了“专业报告”,让 AI 的推荐有了文字依据。但在复盘时我发现,纯文本和表格虽然严谨,却不够直观——用户很难一眼看出房源在“价格”与“通勤”之间的权衡关系。

为了解决这个问题,也为了满足任务书 4.2 节“图表生成时间 ≤1 秒”的性能指标,这一阶段我引入了交互式可视化模块。目标很明确:用雷达图展示六维能力,用柱状图对比通勤成本,并且在架构上坚持“数据预处理缓存 + 声明式绘图”的原则。

1. 选型抉择:Plotly 还是 Altair?

在动手写代码前,我对比了 Streamlit 生态中最主流的两个可视化库:

Altair:声明式语法极其简洁,适合快速出图,但在复杂的交互(如缩放、悬停高亮)上略显单薄。

Plotly:交互体验极佳,自带丰富的工具栏,且对雷达图等复杂图表的支持非常成熟。

考虑到我们需要展示“多维度评分雷达图”,并且希望用户在查看时能进行悬停查看详细分值,我最终选择了 Plotly。它的 graph_objects 能够完美承载任务书 2.4 要求的六大维度(价格、通勤、楼层、配套、户型、装修)。

2. 核心实现:charts.py 的数据归一化与渲染

这是本阶段的逻辑核心。由于雷达图的各个维度量纲完全不同(例如价格是几万,而装修分是0-10),直接绘图会导致坐标轴失真。因此,我在 frontend/app_lib/charts.py 中封装了数据清洗与归一化的逻辑。

设计亮点:

自动归一化:在 prepare_radar_series 函数中,将原始分数统一映射到 0-100 的展示区间,确保雷达图的正多边形视觉比例协调。

性能缓存:利用 @st.cache_data 装饰器对数据预处理过程进行缓存。只要房源列表和权重向量不变,再次渲染图表的时间几乎可以忽略不计,轻松满足“≤1秒”的性能要求。

双图联动:除了雷达图,我还同步实现了通勤时间的分组柱状图,方便用户直观对比不同房源的通勤成本。



import plotly.graph_objects as go
import streamlit as st
from typing import Sequence, Dict, Any
# 雷达图六大维度
RADAR_DIMENSION_KEYS = [
    ("price", "价格"),
    ("commute", "通勤"),
    ("floor", "楼层"),
    ("facilities", "配套"),
    ("layout", "户型"),
    ("decoration", "装修"),
]

def _score_to_0_100(value: float) -> float:

    if value <= 1.0:
        return max(0.0, min(100.0, value * 100.0))
    if value <= 10.0:
        return max(0.0, min(100.0, value * 10.0))
    return max(0.0, min(100.0, value))

@st.cache_data(show_spinner=False)
def cached_radar_series(listings_json: str, weights_json: str) -> list[dict]:
    import json
    listings = json.loads(listings_json)
    weights = json.loads(weights_json)
    
    series = []
    for li in listings:
        raw_scores = {}
        bd = li.get("score_breakdown", {})
        
        # 提取并清洗分数
        for key, label in RADAR_DIMENSION_KEYS:
            # 兼容多种字段名
            raw_key = _canonical_dim_key(key) or key
            if raw_key in bd:
                try:
                    raw_scores[key] = float(bd[raw_key])
                except (TypeError, ValueError):
                    pass
        
        values = [_score_to_0_100(raw_scores.get(key, 0)) for key, _ in RADAR_DIMENSION_KEYS]
        series.append({
            "name": li.get("title", "未知房源"),
            "values": values
        })
    return series

def plot_radar(series: Sequence[Dict]) -> go.Figure:
  
    fig = go.Figure()
    
    for item in series:
        fig.add_trace(go.Scatterpolar(
            r=item["values"],
            theta=[label for _, label in RADAR_DIMENSION_KEYS],
            name=item["name"],
            fill='toself'
        ))

    fig.update_layout(
        polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
        showlegend=True,
        title="房源六维能力雷达图"
    )
    return fig

def plot_commute_bars(rows: list[dict[str, Any]]) -> go.Figure:
    fig = go.Figure()
    
    if not rows:
        fig.update_layout(title="暂无通勤数据")
        return fig

    # 自动检测交通方式(地铁/驾车)
    modes = sorted({r.get("mode", "综合") for r in rows})
    labels = sorted(set(r.get("label") for r in rows))

    for mode in modes:
        y_data = [next((r["minutes"] for r in rows if r.get("label") == label and r.get("mode") == mode), 0) for label in labels]
        fig.add_trace(go.Bar(x=labels, y=y_data, name=mode))

    fig.update_layout(
        barmode="group",
        title="通勤时长对比(分钟)",
        yaxis_title="通勤耗时",
        xaxis_tickangle=-45
    )
    return fig

3. UI 布局与交互反馈

为了不让图表抢占首屏地图和列表的空间,我采用了 st.tabs 标签页策略。

实现逻辑:

分层展示:在主界面设置「房源列表」、「分析报告」、「可视化图表」三个 Tab。用户只有在需要深度对比时才会点击进入图表页,保证了首屏加载的轻快。

性能监控:在图表底部实时显示渲染耗时,确保满足 ≤1s 的硬性指标。

空状态处理:当后端尚未返回评分数据时,图表区域会显示友好的占位提示,而不是报错或留白。

# frontend/app.py — 页面布局实现

import streamlit as st
from app_lib.charts import render_charts_panel

with tab_charts:
    if not processed_listings:
        st.info("暂无房源数据,请先进行筛选或对话推荐。")
    else:
        st.subheader("📊 房源多维可视化")
        
        # 直接调用封装好的图表面板
        render_charts_panel(processed_listings, preferences=user_preferences)
        
        # 内部会显示类似:
        # st.caption(f"图表渲染耗时约 {total_time:.0f} ms;目标 ≤1s")

4. 总结与展望

通过引入 Plotly 和数据缓存机制,我们成功补齐了智能体的“可视化拼图”。

直观性提升:雷达图让用户一眼看清房源的长板与短板,柱状图则量化了通勤痛点。

性能达标:借助 st.cache_data,即使面对几十套房源的批量渲染,前端也能保持丝滑流畅,实测渲染时间控制在 200ms - 600ms 之间。

至此,我们的系统已经具备了“记忆(持久化)、表达(报告)、洞察(图表)”三大能力。

更多推荐