双列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会自动调整画布以适应容器宽度,导致高度被拉伸或压缩
具体解决步骤
固定Plotly图表的渲染尺寸
移除use_container_width=True,让Plotly严格按照设置的height和容器宽度比例渲染,避免响应式调整影响高度。精准计算Matplotlib画布尺寸
根据Plotly的高度(420像素)和Matplotlib默认DPI(100),反向计算画布高度:420 / 100 = 4.2英寸。结合右侧列的宽度比例(左:右=1:2),设置画布宽度为8英寸(保证日历布局紧凑合理)。压缩Matplotlib布局空间
- 关闭
plt.tight_layout(),手动设置子图边距 - 调整Calplot的标题位置,减少垂直空间占用
- 隐藏多余的坐标轴元素
- 关闭
同步容器内元素高度
调整右侧容器内的子标题字体大小,避免额外高度占用,确保左右容器内的元素总高度一致。
修改后的完整代码
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
相关产品推荐
相关产品推荐

