You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

双列Streamlit仪表盘:calplot与Plotly图表高度同步问题

如何在Streamlit中同步Plotly饼图与Calplot日历热图的高度

问题背景

我正在构建一个双列布局的Streamlit仪表盘:

  • 左侧:Plotly饼图
  • 右侧:Calplot实现的日历热图

当前遇到的问题是,即便设置Plotly图表高度为420、Calplot的figsize为(14,14),二者在Streamlit中的显示高度仍无法匹配。尝试过调整figsize、使用tight_layout、调用fig.set_size_inches、移除容器限制、禁用use_container_width等方法,均未解决问题。

示例数据集

data = [ ("2025-01-01", "restaurant"), 
("2025-01-01", "fast-food"), 
("2025-01-02", "restaurant"), 
("2025-01-02", "boulangerie"), 
("2025-01-03", "restaurant"), 
("2025-01-03", "restaurant"), 
("2025-01-04", "fast-food"), 
("2025-01-04", "fast-food"), 
("2025-01-05", "boulangerie"), 
("2025-01-05", "restaurant"), 
("2025-01-06", "restaurant"), 
("2025-01-06", "fast-food"), 
("2025-01-07", "restaurant"), 
("2025-01-07", "boulangerie"), 
("2025-01-08", "fast-food"), 
("2025-01-08", "fast-food"), 
("2025-01-09", "restaurant"), 
("2025-01-09", "restaurant"), 
("2025-01-10", "boulangerie"), 
("2025-01-10", "fast-food"), ] 

原代码

import streamlit as st
import pandas as pd
import plotly.express as px
import calplot
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# -------------------------
# Sample dataset
# -------------------------

df = pd.DataFrame(data, columns=["jour", "type_repas"])
df["jour"] = pd.to_datetime(df["jour"])

st.subheader("Meal types")

col_left, col_right = st.columns([1, 2])

# -------------------------
# Left: Plotly pie chart
# -------------------------
with col_left:

    type_counts = df["type_repas"].value_counts()

    with st.container(border=True):

            fig_pie = px.pie(
                values=type_counts.values,
                names=type_counts.index,
                hole=0.5
            )

            fig_pie.update_traces(
                textinfo="percent",
                textfont_size=18,
                hovertemplate="<b>%{label}</b><br>%{value} repas<br>%{percent}"
            )

            fig_pie.update_layout(
                height=420,

                # fond transparent
                paper_bgcolor="rgba(0,0,0,0)",
                plot_bgcolor="rgba(0,0,0,0)",

                # réduire les marges
                margin=dict(t=20, b=60, l=10, r=10),

                # légende horizontale
                legend=dict(
                    orientation="h",
                    yanchor="top",
                    y=-0.15,
                    xanchor="center",
                    x=0.5
                )
)

            st.plotly_chart(fig_pie, use_container_width=True)

# -------------------------
# Right: Calplot heatmap
# -------------------------
with col_right:

    with st.container(border=True):

            st.subheader("Calendrier des types de repas")

            # -------------------------------------------------
            # Préparation des données
            # -------------------------------------------------

            df_jours = df.copy()

            df_jours["jour"] = pd.to_datetime(df_jours["jour"])

            # -------------------------------------------------
            # Sélection du type de repas
            # -------------------------------------------------

            type_selectionne = st.radio(
                "Choisir un type de repas",
                ["restaurant", "fast-food", "boulangerie"],
                horizontal=True
            )

            # -------------------------------------------------
            # Comptage par jour
            # -------------------------------------------------

            counts = (
                df_jours.groupby(df_jours["jour"].dt.date)["type_repas"]
                .apply(lambda x: (x == type_selectionne).sum())
            )

            counts.index = pd.to_datetime(counts.index)

            # -------------------------------------------------
            # Heatmap calendrier
            # -------------------------------------------------

            import calplot
            import matplotlib.pyplot as plt
            from matplotlib.colors import ListedColormap

            st.subheader(f"{type_selectionne} par jour")

            cmap = ListedColormap([
                "#00000000",  # transparent
                "#ffb74d",
                "#d32f2f"
            ])

            fig, ax = calplot.calplot(
                counts,
                cmap=cmap,
                colorbar=False,
                figsize=(14, 14),
                linewidth=3,
                vmin=0,
                vmax=2
            )

            # -------------------------------------------------
            # Fond transparent
            # -------------------------------------------------

            fig.patch.set_alpha(0)

            for a in fig.axes:
                a.set_facecolor("none")

            plt.tight_layout(pad=0.5)
            
            st.pyplot(fig, transparent=True)


st.divider()

解决方案

核心原因

Plotly和Matplotlib(Calplot基于Matplotlib)在Streamlit中的渲染逻辑存在差异:

  • Plotly的height参数直接控制图表的实际显示像素高度
  • Matplotlib图表的figsize是英寸单位的画布尺寸,需结合DPI计算实际像素高度,且Streamlit会自动调整画布以适应容器宽度,导致高度被拉伸或压缩

具体解决步骤

  1. 固定Plotly图表的渲染尺寸
    移除use_container_width=True,让Plotly严格按照设置的height和容器宽度比例渲染,避免响应式调整影响高度。

  2. 精准计算Matplotlib画布尺寸
    根据Plotly的高度(420像素)和Matplotlib默认DPI(100),反向计算画布高度:420 / 100 = 4.2英寸。结合右侧列的宽度比例(左:右=1:2),设置画布宽度为8英寸(保证日历布局紧凑合理)。

  3. 压缩Matplotlib布局空间

    • 关闭plt.tight_layout(),手动设置子图边距
    • 调整Calplot的标题位置,减少垂直空间占用
    • 隐藏多余的坐标轴元素
  4. 同步容器内元素高度
    调整右侧容器内的子标题字体大小,避免额外高度占用,确保左右容器内的元素总高度一致。

修改后的完整代码

import streamlit as st
import pandas as pd
import plotly.express as px
import calplot
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# -------------------------
# Sample dataset
# -------------------------

data = [ ("2025-01-01", "restaurant"), 
("2025-01-01", "fast-food"), 
("2025-01-02", "restaurant"), 
("2025-01-02", "boulangerie"), 
("2025-01-03", "restaurant"), 
("2025-01-03", "restaurant"), 
("2025-01-04", "fast-food"), 
("2025-01-04", "fast-food"), 
("2025-01-05", "boulangerie"), 
("2025-01-05", "restaurant"), 
("2025-01-06", "restaurant"), 
("2025-01-06", "fast-food"), 
("2025-01-07", "restaurant"), 
("2025-01-07", "boulangerie"), 
("2025-01-08", "fast-food"), 
("2025-01-08", "fast-food"), 
("2025-01-09", "restaurant"), 
("2025-01-09", "restaurant"), 
("2025-01-10", "boulangerie"), 
("2025-01-10", "fast-food"), ] 

df = pd.DataFrame(data, columns=["jour", "type_repas"])
df["jour"] = pd.to_datetime(df["jour"])

st.subheader("Meal types")

col_left, col_right = st.columns([1, 2])

# -------------------------
# Left: Plotly pie chart
# -------------------------
with col_left:
    type_counts = df["type_repas"].value_counts()
    with st.container(border=True):
        fig_pie = px.pie(
            values=type_counts.values,
            names=type_counts.index,
            hole=0.5
        )

        fig_pie.update_traces(
            textinfo="percent",
            textfont_size=18,
            hovertemplate="<b>%{label}</b><br>%{value} repas<br>%{percent}"
        )

        fig_pie.update_layout(
            height=420,
            width=None,  # 让宽度自适应列比例
            paper_bgcolor="rgba(0,0,0,0)",
            plot_bgcolor="rgba(0,0,0,0)",
            margin=dict(t=20, b=60, l=10, r=10),
            legend=dict(
                orientation="h",
                yanchor="top",
                y=-0.15,
                xanchor="center",
                x=0.5
            )
        )
        # 移除use_container_width,固定高度
        st.plotly_chart(fig_pie, use_container_width=False)

# -------------------------
# Right: Calplot heatmap
# -------------------------
with col_right:
    with st.container(border=True):
        # 调整子标题字体大小,减少高度占用
        st.markdown("<h3 style='font-size:18px; margin-bottom:10px;'>Calendrier des types de repas</h3>", unsafe_allow_html=True)

        df_jours = df.copy()
        df_jours["jour"] = pd.to_datetime(df_jours["jour"])

        type_selectionne = st.radio(
            "Choisir un type de repas",
            ["restaurant", "fast-food", "boulangerie"],
            horizontal=True
        )

        counts = (
            df_jours.groupby(df_jours["jour"].dt.date)["type_repas"]
            .apply(lambda x: (x == type_selectionne).sum())
        )
        counts.index = pd.to_datetime(counts.index)

        # 调整子标题字体大小
        st.markdown(f"<h3 style='font-size:18px; margin-bottom:10px;'>{type_selectionne} par jour</h3>", unsafe_allow_html=True)

        cmap = ListedColormap([
            "#00000000",  # transparent
            "#ffb74d",
            "#d32f2f"
        ])

        # 根据Plotly高度计算Matplotlib画布尺寸:420像素 / 100 DPI = 4.2英寸
        fig, ax = calplot.calplot(
            counts,
            cmap=cmap,
            colorbar=False,
            figsize=(8, 4.2),  # 宽度适配右侧列,高度匹配Plotly
            linewidth=3,
            vmin=0,
            vmax=2,
            suptitle=None  # 关闭自动生成的总标题
        )

        # 透明背景设置
        fig.patch.set_alpha(0)
        for a in fig.axes:
            a.set_facecolor("none")
        
        # 手动设置紧凑边距,替代tight_layout
        plt.subplots_adjust(top=0.9, bottom=0.1, left=0.05, right=0.95)
        
        # 传递DPI参数,确保高度计算准确
        st.pyplot(fig, transparent=True, dpi=100)

st.divider()

内容的提问来源于stack exchange,提问作者Av65

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 03:17:27