Plotly/Dash直方图分箱hovertemplate自定义格式化展示钢琴练习时长
解决方案
有两种可行实现方式,你可以根据自己的需求选择:
方案1:最小改动现有直方图逻辑
核心利用Plotly内置的时间格式化能力,仅需修改几行代码即可实现需求:
改动点:
- SQL查询直接读取秒级时长,避免分钟转秒的精度损失
- 将y值乘以1000转为毫秒时间戳格式,即可通过Plotly的strftime格式化符直接渲染时分秒
- 如需y轴也展示时分秒,新增y轴
tickformat配置即可
修改后核心代码片段:
# 改动1:查询直接取秒 cur.execute("select started date, seconds FROM practice") result = cur.fetchall() con.close() df = pd.DataFrame(result, columns=['date', 'seconds']) data = [dict( x=df['date'], # 改动2:y值转毫秒,适配Plotly时间格式化规则 y=df['seconds'] * 1000, marker=dict(color='rgb(68, 68, 68)'), name='date', type='histogram', histfunc="sum", xbins=dict( size='1D', start='1983-12-31 00:00', end='2026-12-31 00:00', ), # 改动3:hover模板用时间格式渲染 hovertemplate= '%{y|%H:%M:%S} 练习时长<br>' + '%{x}<br><extra></extra>' )] layout = dict( # 其他配置不变,新增y轴tickformat可让y轴也显示时分秒,不需要可以删掉 yaxis=dict( title='练习时长', type='linear', tickformat='%H:%M:%S' ), # 原有updatemenus配置不变 )
注意:该方案如果单日练习时长超过24小时,%H格式化会自动取模显示为0-23区间值,无法正确显示超过24小时的总时长,如果有超过24小时的统计需求请使用方案2。
方案2:改用预聚合柱状图(更灵活,推荐)
直方图的自动聚合逻辑限制较多,你可以放弃直方图,改用pandas提前按选中的时间粒度聚合数据,自定义格式化时分秒后放到customdata中,完全可控hover展示内容。需要新增一个Dash回调触发聚合逻辑,替换原有restyle改xbins的实现:
完整修改后代码:
import sqlite3 import dash from dash import dcc, html, Input, Output import pandas as pd from datetime import timedelta app = dash.Dash(__name__) # 提前加载基础数据避免每次请求查库 con = sqlite3.connect('practicetracker.db') df = pd.read_sql("select started as date, seconds FROM practice", con, parse_dates=['date']) con.close() df = df.set_index('date') def format_seconds(sec): # 自定义格式化函数,支持超过24小时的时长展示 td = timedelta(seconds=int(sec)) hours = td.days * 24 + td.seconds // 3600 minutes = (td.seconds % 3600) // 60 seconds = td.seconds % 60 return f"{hours:02d}:{minutes:02d}:{seconds:02d}" app.layout = html.Div([ dcc.RadioItems( id='time-granularity', options=[ {'label': '小时', 'value': 'H'}, {'label': '天', 'value': 'D'}, {'label': '周', 'value': 'W'}, {'label': '月', 'value': 'M'} ], value='D', style={'position': 'absolute', 'top': 10, 'right': 10} ), dcc.Graph(id='practice-fig') ]) @app.callback( Output('practice-fig', 'figure'), Input('time-granularity', 'value') ) def update_fig(gran): # 按选中粒度聚合 agg_df = df.resample(gran).sum().reset_index() # 格式化时分秒存入customdata agg_df['format_time'] = agg_df['seconds'].apply(format_seconds) return { 'data': [ { 'x': agg_df['date'], 'y': agg_df['seconds'], 'type': 'bar', 'marker': {'color': 'rgb(68, 68, 68)'}, 'customdata': agg_df['format_time'], 'hovertemplate': '%{customdata} 练习时长<br>%{x}<br><extra></extra>' } ], 'layout': { 'paper_bgcolor': 'rgb(240, 240, 240)', 'plot_bgcolor': 'rgb(240, 240, 240)', 'title': '<b>钢琴练习时长</b>', 'bargap': 0.2, 'yaxis': { 'title': '练习时长(秒)' # 也可以自定义y轴tick为时分秒格式,按需调整即可 } } } if __name__ == '__main__': app.run_server(host="0.0.0.0", debug=True)
内容的提问来源于stack exchange,提问作者Felix Auer
相关产品推荐
相关产品推荐

