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

如何使用Python Plotly库在点击图表空白区域时触发回调?

Plotly K线图检测空白区域点击的实现方案

核心思路

Plotly默认只响应数据点的点击,要检测空白区域,需要先捕获所有点击事件,再判断点击位置是否落在K线数据的范围内——若未命中任何K线,则判定为空白点击,执行后续操作。


方法一:Plotly.js(前端)实现

通过监听plotly_click事件获取点击坐标,遍历K线数据对比范围:

// 获取图表容器
const chart = document.getElementById('candlestick-chart');

// 监听点击事件
chart.on('plotly_click', function(eventData) {
  const clickX = eventData.points[0].x; // 点击的时间轴数值
  const clickY = eventData.points[0].y; // 点击的价格轴数值
  const candlestickData = eventData.data[0]; // K线数据系列

  let isOnKline = false;
  // 遍历每根K线,判断点击位置是否落在K线范围内
  for (let i = 0; i < candlestickData.x.length; i++) {
    const currX = candlestickData.x[i];
    // 计算当前K线的时间边界(取相邻K线的中间值)
    const nextX = candlestickData.x[i+1] || currX + (currX - candlestickData.x[i-1]);
    const xMin = currX - (nextX - currX)/2;
    const xMax = currX + (nextX - currX)/2;
    // K线的价格边界是low到high
    const yMin = candlestickData.low[i];
    const yMax = candlestickData.high[i];

    if (clickX >= xMin && clickX <= xMax && clickY >= yMin && clickY <= yMax) {
      isOnKline = true;
      break;
    }
  }

  // 未命中任何K线,执行空白点击操作
  if (!isOnKline) {
    console.log(`在价格${clickY}处添加提醒`);
    // 这里可以添加绘制水平线、弹出提醒框等逻辑
  }
});

方法二:Dash(Python)实现

Dash中clickData仅返回数据点点击信息,需添加透明散点层覆盖整个图表区域,确保任何位置点击都能触发事件,再判断是否命中K线:

import dash
from dash import dcc, html, Input, Output, State
import plotly.graph_objects as go
import pandas as pd

# 模拟K线数据
df = pd.DataFrame({
    'x': pd.date_range(start='2024-01-01', periods=10),
    'open': [100, 102, 101, 105, 104, 106, 108, 107, 109, 110],
    'high': [103, 104, 103, 106, 105, 107, 110, 109, 111, 112],
    'low': [99, 101, 99, 103, 102, 104, 106, 105, 107, 108],
    'close': [102, 101, 103, 104, 106, 107, 109, 108, 110, 111]
})

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Graph(id='candlestick-chart'),
    html.Div(id='alert-info')
])

@app.callback(
    Output('candlestick-chart', 'figure'),
    Input('candlestick-chart', 'clickData'),
    State('candlestick-chart', 'figure')
)
def update_chart(clickData, fig):
    if clickData:
        click_x = clickData['points'][0]['x']
        click_y = clickData['points'][0]['y']
        candlestick_data = fig['data'][0]
        is_on_kline = False

        # 遍历K线判断点击位置
        for i in range(len(candlestick_data['x'])):
            curr_x = pd.Timestamp(candlestick_data['x'][i])
            next_x = pd.Timestamp(candlestick_data['x'][i+1]) if i+1 < len(candlestick_data['x']) else curr_x + pd.Timedelta(days=1)
            x_min = curr_x - (next_x - curr_x)/2
            x_max = curr_x + (next_x - curr_x)/2
            y_min = candlestick_data['low'][i]
            y_max = candlestick_data['high'][i]

            if pd.Timestamp(click_x) >= x_min and pd.Timestamp(click_x) <= x_max and click_y >= y_min and click_y <= y_max:
                is_on_kline = True
                break

        # 空白点击,添加价格提醒水平线
        if not is_on_kline:
            fig['data'].append(go.Scatter(
                x=[fig['layout']['xaxis']['range'][0], fig['layout']['xaxis']['range'][1]],
                y=[click_y, click_y],
                mode='lines',
                line=dict(color='red', dash='dash'),
                name=f'提醒价格: {click_y}'
            ))
    else:
        # 初始化图表,添加透明散点层
        fig = go.Figure(data=[
            go.Candlestick(
                x=df['x'], open=df['open'], high=df['high'], low=df['low'], close=df['close']
            ),
            go.Scatter(
                x=df['x'],
                y=[df['low'].min()] * len(df['x']),
                mode='markers',
                marker=dict(size=0, opacity=0),  # 完全透明不可见
                hoverinfo='none'
            )
        ])
        fig.update_layout(xaxis_rangeslider_visible=False)
    return fig

@app.callback(
    Output('alert-info', 'children'),
    Input('candlestick-chart', 'clickData'),
    State('candlestick-chart', 'figure')
)
def show_alert(clickData, fig):
    if clickData:
        click_y = clickData['points'][0]['y']
        candlestick_data = fig['data'][0]
        is_on_kline = False

        for i in range(len(candlestick_data['x'])):
            curr_x = pd.Timestamp(candlestick_data['x'][i])
            next_x = pd.Timestamp(candlestick_data['x'][i+1]) if i+1 < len(candlestick_data['x']) else curr_x + pd.Timedelta(days=1)
            x_min = curr_x - (next_x - curr_x)/2
            x_max = curr_x + (next_x - curr_x)/2
            y_min = candlestick_data['low'][i]
            y_max = candlestick_data['high'][i]

            if pd.Timestamp(clickData['points'][0]['x']) >= x_min and pd.Timestamp(clickData['points'][0]['x']) <= x_max and click_y >= y_min and click_y <= y_max:
                is_on_kline = True
                break

        if not is_on_kline:
            return f"已在价格 {click_y} 处添加提醒"
    return ""

if __name__ == '__main__':
    app.run_server(debug=True)

注意事项

  • 时间轴边界判断需匹配实际数据类型(时间戳、字符串等)
  • 透明散点层的y值需覆盖图表完整价格范围,确保所有位置点击都能触发事件
  • 若图表支持缩放,需在回调中获取当前x/y轴范围,调整判断逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 19:20:09