返回

Plotly 子图交互:静态 HTML 实现选中高亮 (3种方法)

python

Plotly 子图交互:实现选中与取消选中效果

处理多个 Plotly 子图(subplots)时,你可能会遇到一个痛点:图太多,屏幕放不下,想仔细看某个子图就得不停地上下滚动,对比起来很不方便。理想的情况是,用户能够点击某个或某几个子图,让它们突出显示或者放大,方便细看和比较,操作完成后又能恢复原状。

这个需求听起来直接,但如果你的最终产出物必须是静态 HTML 文件 ,事情就变得稍微复杂了。因为丰富的交互(比如动态改变布局大小、隐藏/显示元素)通常需要后端服务器或者更复杂的客户端逻辑(像 Dash 应用那样),而纯粹的静态 HTML 文件本身不具备这样的动态处理能力。Plotly 生成的 HTML 文件虽然内嵌了 JavaScript 用于图表本身的交互(如缩放、悬停),但默认并不包含这种“选中/取消选中子图并改变布局”的功能。

为什么直接实现有难度?

Plotly 生成的图表是 HTML 和 JavaScript 的组合。图表的布局(包括子图的位置、大小)是在生成 HTML 文件时基本确定的。要在用户点击后动态改变某个子图的大小或视觉样式(比如加个边框、让其他子图变暗),并且还要能恢复,这需要额外的 JavaScript 代码来操作 HTML 的 DOM(文档对象模型)和 CSS 样式。Plotly 本身不直接提供用于这种特定“子图选择”交互的高级 API。

虽然 Dash 框架能轻松实现这类交互,但 Dash 应用通常需要一个运行中的 Python 服务器来处理回调(callback),用于响应用户的操作并更新图表。将一个完整的、依赖后端回调的 Dash 应用导出为单个、完全离线的静态 HTML 文件是有限制的,难以保留全部交互性。

不过,别灰心,我们还是有办法在静态 HTML 的限制下,模拟或者实现类似的效果。

可行方案

下面介绍几种在静态 HTML 文件中实现或模拟 Plotly 子图选择效果的方法。

方案一:利用 JavaScript 和 CSS 实现视觉选中

这是最接近“选中”效果,并且能在纯静态 HTML 中实现的方案。思路是:

  1. 用 Plotly 正常生成带子图的 Figure 对象。
  2. 将 Figure 导出为 HTML,但只导出核心的图表 <div> 部分,并且引入 Plotly.js 库(推荐用 CDN)。
  3. 编写额外的 JavaScript 代码,监听子图区域的点击事件。
  4. 当用户点击某个子图时,用 JavaScript 给该子图对应的 HTML 元素(通常是一个 <div><g> 元素)添加一个特殊的 CSS 类(比如 selected-subplot)。
  5. 编写对应的 CSS 规则,定义 selected-subplot 类的样式(例如,添加明显的边框、阴影,或者稍微增加一点尺寸——注意这可能影响布局)。
  6. 再次点击时,移除该 CSS 类,恢复原样。或者实现更复杂的逻辑,比如允许选中多个,或者点击空白处取消所有选中。

原理和作用:

这种方法不改变 Plotly 图表本身的布局结构(子图位置、原始大小基本不变),而是通过 CSS “视觉强调”被选中的子图。它完全在浏览器端执行,不依赖服务器,因此适用于静态 HTML。

操作步骤与代码示例:

  1. Python (生成 Plotly Figure):
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

# 创建带子图的 Figure
rows, cols = 2, 3
fig = make_subplots(rows=rows, cols=cols, subplot_titles=[f'Subplot {i+1}' for i in range(rows*cols)])

# 添加一些示例轨迹 (trace)
for i in range(1, rows + 1):
    for j in range(1, cols + 1):
        fig.add_trace(go.Scatter(
            x=np.arange(10),
            y=np.random.randn(10) + (i * cols + j), # 示例数据
            mode='lines+markers',
            name=f'Trace {i},{j}'
        ), row=i, col=j)

fig.update_layout(
    title_text="Plotly Subplots - Click to Select",
    height=600,
    width=900,
    showlegend=False # 通常子图很多时隐藏图例
)

# 导出图表的 HTML 'div' 部分,并指定 Plotly.js 来源
# include_plotlyjs='cdn' 会自动包含 CDN 链接,方便嵌入
# full_html=False 只生成包含图表的 div
# config={'staticPlot': False} 确保 Plotly.js 交互开启
config = {'staticPlot': False}
plot_div = fig.to_html(full_html=False, include_plotlyjs='cdn', config=config)

# 注意:这里我们只拿到了 <div id="..." class="plotly-graph-div" ...></div> 这部分
# 我们需要手动把它嵌入到一个完整的 HTML 结构中
  1. HTML & JavaScript (整合与交互逻辑):

创建一个 HTML 文件(例如 interactive_subplots.html),将上面生成的 plot_div 粘贴进去,并添加自定义的 CSS 和 JavaScript。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    
    <style>
        /* CSS for highlighting selected subplots */
        .selected-subplot {
            outline: 3px solid blue !important; /* 使用 !important 确保覆盖 Plotly 默认样式 */
            /* 可以添加其他效果, 比如轻微的 box-shadow */
            box-shadow: 0 0 10px rgba(0, 0, 255, 0.5);
            /* 如果想尝试放大,需要小心处理布局问题,可能需要 position: relative 和 z-index */
            /* transform: scale(1.05); */
            /* z-index: 10; */
        }

        /* 可以给其他未选中的子图添加效果,例如变暗 */
        .subplot-container.dimmed .subplot:not(.selected-subplot) {
             opacity: 0.5;
             transition: opacity 0.3s ease-in-out;
        }
        .subplot {
             transition: opacity 0.3s ease-in-out; /* 让恢复时也有过渡效果 */
        }

    </style>
</head>
<body>

    <h1>Plotly Subplots Example</h1>
    <p>Click on a subplot to select/deselect it.</p>

    <!-- 将 Python 生成的 div 粘贴在这里 -->
    <div id="plot-container">
        {{ plot_div | safe }} <!-- 如果使用模板引擎,这样插入 -->
        <!-- 或者直接粘贴 Python 输出的那个 <div...>...</div> -->
        <!-- 示例: -->
        <!-- <div id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" class="plotly-graph-div" style="height:600px; width:900px;"></div> -->
        <!-- 注意 ID 是动态生成的,你需要替换成你实际生成的 ID -->
    </div>


    <script>
        // 等待 Plotly 图表渲染完成
        // Plotly 图表渲染是异步的,直接在 <script> 标签里可能太早
        // 一种简单的方式是用 setTimeout 延迟执行,或者监听 Plotly 的 'plotly_afterplot' 事件
        // 但更可靠的方式是等 DOM 加载完成,并假设 Plotly 初始化需要一点时间

        window.onload = function() {
            // 稍微延迟一点,确保 Plotly 绘制完成(这不是最完美的,但简单有效)
            setTimeout(attachClickListener, 500);
        };

        function attachClickListener() {
            // 找到 Plotly 图表的根 div
            const plotDiv = document.querySelector('.plotly-graph-div'); // 假设页面只有一个图
            if (!plotDiv || !plotDiv.querySelector('.subplot')) {
                 console.error("Plotly div or subplots not found. Retrying...");
                 setTimeout(attachClickListener, 500); // 简单重试
                 return;
            }

            // 获取所有的子图元素
            // Plotly 对子图区域通常会用类似 'subplot xy', 'subplot x2y2' 这样的类名
            // 它们通常是 <g> 元素包裹在一个 <svg> 里,或者有时是最外层的 <div>
            // 检查生成的 HTML 结构确定准确的选择器
            const subplots = plotDiv.querySelectorAll('.subplot'); // Plotly 可能会变,需要检查DOM结构
            const subplotContainer = plotDiv.querySelector('svg > .main-svg'); // 一个可能的容器,用于管理 dimmed 效果

            if (subplots.length === 0) {
                 console.warn("No elements with class 'subplot' found. Click selection might not work correctly.");
                 // 可以尝试其他选择器,例如基于 transform 属性的 <g> 元素
                 // const subplots = plotDiv.querySelectorAll('g.cartesianlayer'); // 这更可能是绘图层
                 return; // 如果找不到,就不能进行下去
            }

            console.log(`Found ${subplots.length} subplot elements.`);

             // 给每个子图添加点击事件监听器
             subplots.forEach(subplot => {
                 // Plotly 会阻止某些事件冒泡,可能需要监听 'plotly_click' 事件,但这更复杂
                 // 直接监听 DOM 元素的 click 通常对背景区域有效
                 subplot.addEventListener('click', function(event) {
                    event.stopPropagation(); // 阻止事件冒泡,避免影响 Plotly 的其他交互

                    const isSelected = this.classList.contains('selected-subplot');

                    // 实现单选效果:先移除所有其他子图的选中状态
                    // subplots.forEach(sp => sp.classList.remove('selected-subplot'));

                    // 切换选中状态
                    if (isSelected) {
                        this.classList.remove('selected-subplot');
                        // 如果添加了 dimmed 效果,点击取消选中时移除 dimmed
                         if(subplotContainer) subplotContainer.classList.remove('dimmed');
                    } else {
                        // 实现单选: 先清除其他的
                        subplots.forEach(sp => sp.classList.remove('selected-subplot'));

                        this.classList.add('selected-subplot');
                        // 可选:让其他子图变暗
                         if(subplotContainer) subplotContainer.classList.add('dimmed');
                    }

                    // 如果想支持多选 (例如按住 Ctrl/Cmd 键点击):
                    // if (event.ctrlKey || event.metaKey) {
                    //     this.classList.toggle('selected-subplot');
                    // } else {
                    //     // 单击行为:只选中当前这个,取消其他所有
                    //     const currentlySelected = this.classList.contains('selected-subplot');
                    //     subplots.forEach(sp => sp.classList.remove('selected-subplot'));
                    //     if (!currentlySelected) { // 如果之前没选中,现在要选中它
                    //          this.classList.add('selected-subplot');
                    //     }
                    // }
                 });
             });

            // 可选: 添加一个点击图表空白处取消所有选中的功能
            plotDiv.addEventListener('click', function(event) {
                 // 检查点击是否发生在子图元素之外
                 let clickedOnSubplot = false;
                 subplots.forEach(sp => {
                     if (sp.contains(event.target) || sp === event.target) {
                         clickedOnSubplot = true;
                     }
                 });

                 if (!clickedOnSubplot) {
                     subplots.forEach(sp => sp.classList.remove('selected-subplot'));
                     if (subplotContainer) subplotContainer.classList.remove('dimmed');
                 }
             });
        }

    </script>

</body>
</html>

注意:

  • 你需要将 Python 生成的 plot_div 字符串实际插入到 HTML 文件中标记的位置。如果你使用 Flask/Django 等 Web 框架,可以通过模板变量传递。如果是纯静态,就手动复制粘贴。
  • Plotly 内部 HTML 结构和类名可能会随着版本更新而变化。上面代码中的 .subplot 选择器是一个假设,你必须 在浏览器中使用开发者工具(Inspect Element)检查你生成的 Plotly 图表的实际 DOM 结构,找到代表每个子图区域的稳定、可用的 CSS 选择器。可能是 g.subplot, div.subplot, 或者其他基于 idclass 的选择器。
  • window.onloadsetTimeout 是确保 Plotly 渲染完成后再执行 JS 的简单方法,对于复杂页面或网络较慢时可能不够鲁棒。更健壮的方法是使用 Plotly 的 plotly_afterplot 事件,但这需要更深入地集成 Plotly 的 JavaScript API。
  • CSS 中的 !important 可能需要用来覆盖 Plotly 的内联样式或默认样式。

安全建议:

此方案主要操作的是 DOM 和 CSS,风险较低。主要是确保你的 JavaScript 代码不会意外干扰 Plotly 自身的交互功能,并且不要引入外部不可信的脚本。

进阶使用:

  • 多选: 修改 JS 逻辑,例如结合 CtrlShift 键来实现多选。
  • 动态内容加载: 可以结合 Ajax 或其他技术,在选中子图后加载更详细的数据或注解。
  • 平滑过渡: 使用 CSS transition 属性让选中/取消选中的视觉效果(如边框、透明度)更平滑。

方案二:JavaScript 控制显示/隐藏与调整大小(更接近“聚焦”)

这种方法更进一步,尝试在用户点击后隐藏其他子图,并将选中的子图放大显示。这比纯视觉样式修改要复杂,因为会改变布局。

原理和作用:

点击某个子图后,用 JavaScript 修改其他子图容器的 CSS display 属性为 none,同时调整被选中子图容器的 width, height, position 等属性,使其占据更大空间。再次点击或点击其他地方恢复原状。

操作步骤与代码示例:

基础结构类似方案一,但 JavaScript 和 CSS 的逻辑不同。

  1. Python & HTML 结构: 同方案一,生成 Figure 并嵌入 HTML。可能需要在外层包裹每个子图(或整个图表)的容器 div 以方便控制。

  2. CSS (样式):

.plot-wrapper { /* 一个包裹 Plotly 图表 div 的容器 */
    position: relative; /* 为了子元素绝对定位 */
}

.subplot-interactive { /* 可能需要给子图元素添加的类,或者操作 Plotly 内部元素 */
    transition: all 0.3s ease-in-out; /* 平滑过渡效果 */
    cursor: pointer;
}

.subplot-focused {
    position: absolute; /* 或者 'fixed',根据需要 */
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); /* 居中 */
    width: 80vw !important;  /* 使用 viewport 单位或者固定像素 */
    height: 80vh !important;
    z-index: 1000; /* 确保在最上层 */
    background-color: white; /* 防止透视下面内容 */
    border: 1px solid #ccc;
    box-shadow: 0 0 15px rgba(0,0,0,0.3);
    /* 需要 Plotly.Plots.resize(divId) 来强制重绘 */
}

.hidden-subplot {
    display: none !important;
}

.parent-container-hiding > :not(.subplot-focused) {
    opacity: 0;
    pointer-events: none; /* 隐藏期间不可交互 */
}
  1. JavaScript (逻辑):
// ... (window.onload, 获取 plotDiv, subplots 同方案一) ...

let focusedSubplot = null;
const plotDiv = document.querySelector('.plotly-graph-div'); // 主图表 Div
const parentContainer = document.getElementById('plot-container'); // 外部容器

// 需要一个更可靠的方法来识别哪个 DOM 元素对应哪个子图
// 这非常依赖 Plotly 的内部结构。假设每个子图的主要绘图区是一个 <g class="subplot">
const subplots = plotDiv.querySelectorAll('.subplot'); // 示例选择器, 需要验证!

subplots.forEach((subplot, index) => {
    subplot.classList.add('subplot-interactive'); // 方便选择和添加样式
    subplot.dataset.subplotIndex = index; // 存个索引

    subplot.addEventListener('click', function(event) {
        event.stopPropagation();
        const subplotElement = this; // 被点击的子图元素

        if (focusedSubplot === subplotElement) {
            // 如果点击的是已聚焦的子图,则恢复
            resetLayout();
        } else {
            // 聚焦当前子图
            focusOnSubplot(subplotElement);
        }
    });
});

// 点击容器空白处恢复
parentContainer.addEventListener('click', function(event) {
    if (focusedSubplot && event.target === parentContainer) { // 确保点在容器上,而不是子图内部
        resetLayout();
    }
});

function focusOnSubplot(subplotElement) {
    // 移除之前的聚焦状态
    if (focusedSubplot) {
        focusedSubplot.classList.remove('subplot-focused');
    }

    // 隐藏所有子图
    parentContainer.classList.add('parent-container-hiding'); // 给父容器加状态,用于隐藏其他子图
    subplots.forEach(sp => {
         if (sp !== subplotElement) {
             sp.classList.add('hidden-subplot'); // 这个可能没用,如果父级opacity为0了
         }
    });

    // 聚焦当前子图
    subplotElement.classList.add('subplot-focused');
    focusedSubplot = subplotElement;

    // 关键: 需要告诉 Plotly 调整大小!
    // Plotly 需要知道它的容器尺寸变化了才能重绘。
    // 如果是直接改变 Plotly <div id="..."> 的大小,调用 Plotly.Plots.resize(divId)
    // 如果是改变内部 SVG/g 元素的大小,这通常更复杂,可能无法简单重绘
    // 这个方案假设你能改变包含整个Plotly图的外部div大小,或者用JS模拟Modal效果
    // 最好是改变 Plotly graph div 本身的大小, 但这会影响内部所有子图的相对布局

    // 简化的做法:把这个子图的内容“弹出”到一个新的临时容器中放大展示?
    // 这就超出了简单的 CSS 调整,需要克隆节点或用 modal 库。

    // 强制 Plotly 重绘 (如果改变了 Plotly 容器 div 的大小)
    try {
        Plotly.Plots.resize(plotDiv);
    } catch (e) {
        console.error("Error resizing Plotly plot:", e);
    }
}

function resetLayout() {
    if (focusedSubplot) {
        focusedSubplot.classList.remove('subplot-focused');
        focusedSubplot = null;
    }
    parentContainer.classList.remove('parent-container-hiding');
    subplots.forEach(sp => {
        sp.classList.remove('hidden-subplot');
    });

    // 同样需要 resize Plotly 图表,恢复其原始容器大小
     try {
        Plotly.Plots.resize(plotDiv);
    } catch (e) {
        console.error("Error resizing Plotly plot:", e);
    }
}

挑战与注意点:

  • 动态调整大小与重绘: 这是此方案最大的难点。简单地用 CSS 改变子图元素(如 <g>)的大小,Plotly 图内部的坐标轴、线条等不会 自动重新计算和绘制以适应新尺寸。你需要改变包含整个 Plotly 图表的 <div> 的大小,然后调用 Plotly.Plots.resize(graphDiv) 来触发 Plotly 的重绘逻辑。这会重绘 整个 图表,而不仅仅是那个子图。
  • 布局破坏: 绝对定位或固定定位放大子图会脱离原来的文档流,可能会遮挡页面其他内容。隐藏其他子图会改变原有的网格布局。恢复时也要确保布局正确还原。
  • 识别子图元素: 同方案一,准确识别代表每个子图并可以安全操作的 DOM 元素至关重要,且依赖 Plotly 的内部实现。
  • 性能: 频繁的 DOM 操作和 Plotly 重绘可能在高复杂度图表上引起性能问题。

进阶使用:

  • Modal 弹窗: 与其在原地放大,不如点击后用 JavaScript 创建一个模态框(Modal/Lightbox),在模态框里用 Plotly 重新绘制一个只包含该子图(或者选定几个子图)的新图表实例。这提供了更干净的“聚焦”视图,并且更容易控制大小和布局。需要 Plotly.react() 或 Plotly.newPlot() 在新容器中绘图。
  • 动画效果: 使用 CSS transition 或 JavaScript 动画库让放大、缩小、隐藏、显示的过程更平滑。

方案三:Dash Client-Side Callback (需要特殊导出)

虽然标准 Dash 应用不适合静态导出,但 Dash 支持客户端回调 (clientside_callback)。客户端回调允许你在浏览器端直接用 JavaScript 响应事件并修改图表(或其他组件)的属性,而无需与 Python 后端通信。

原理和作用:

构建一个 Dash 应用,但交互逻辑(如根据点击事件修改图表的 figure 数据或布局)完全由写在 assets 文件夹下的 JavaScript 函数处理。这样的 Dash 应用在某些情况下可以通过特定工具(如 dash-generate-components 或第三方库/技术)打包成近乎静态的单页面应用,或者至少其核心交互能在无后端时运行。

操作步骤:

  1. 创建 Dash App:

    import dash
    from dash import dcc, html, Input, Output, clientside_callback
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    import numpy as np
    import json # 用于在客户端和服务器端传递数据
    
    app = dash.Dash(__name__)
    
    # ... (创建 fig 对象,同方案一) ...
    # 例如:
    rows, cols = 2, 3
    fig = make_subplots(rows=rows, cols=cols, subplot_titles=[f'Subplot {i+1}' for i in range(rows*cols)])
    for i in range(1, rows + 1):
       for j in range(1, cols + 1):
           fig.add_trace(go.Scatter(x=np.arange(10), y=np.random.randn(10)+(i*cols+j), mode='lines+markers', name=f'Trace {i},{j}'), row=i, col=j)
    fig.update_layout(title_text="Dash Client-Side Subplot Interaction", height=600, width=900, showlegend=False)
    
    app.layout = html.Div([
        html.H1("Dash Client-Side Callback Example"),
        dcc.Graph(id='subplot-graph', figure=fig),
        # 存储当前选中的子图信息 (例如索引)
        dcc.Store(id='selected-subplot-info', data={'selectedIndex': None})
    ])
    
    # 定义 Client-side Callback
    # 监听 Graph 的 clickData,输出更新后的 figure 或 更新 dcc.Store
    clientside_callback(
        """
        function(clickData, currentFig, storedData) {
            if (!clickData) {
                // 没有点击数据,可能是在取消选择或初始加载
                // 如果需要取消选择逻辑,在这里处理
                 if (storedData && storedData.selectedIndex !== null) {
                     // 取消高亮 (修改图形属性)
                    let newFig = Object.assign({}, currentFig); // 创建副本避免直接修改 state
                    try {
                       // 找出之前选中的子图对应的 trace/layout 并恢复
                       // ... (逻辑复杂,取决于你想如何表示“选中”) ...
                       // 比如:恢复所有 trace 的透明度
                        newFig.data.forEach(trace => trace.opacity = 1);
                    } catch (e) { console.error('Error resetting figure:', e); }
                    storedData.selectedIndex = null; // 重置存储
                    return [newFig, storedData];
                 }
                return dash_clientside.no_update; // 无点击则不更新
            }
    
            // 获取点击发生在哪条曲线上 (clickData.points[0].curveNumber)
            // 从 curveNumber 推断子图索引可能比较 tricky, 依赖 trace 添加顺序
            // 或者更简单:在 clickData 中查找 subplot 相关信息(如果有的话)
            // Plotly 的 clickData 结构需要具体分析
    
            const point = clickData.points[0];
            const curveIndex = point.curveNumber;
            // 假设我们能从 curveIndex 映射到子图索引 (例如,如果每个子图只有一个 trace)
            const subplotIndex = curveIndex; // 这只是一个简化的假设!
    
            let newFig = Object.assign({}, currentFig); // 操作副本
    
            // 实现选中效果: 例如,降低其他所有 trace 的透明度
            newFig.data = newFig.data.map((trace, index) => {
                if (index === curveIndex) {
                    trace.opacity = 1.0; // 选中的不透明
                } else {
                    trace.opacity = 0.3; // 其他的半透明
                }
                return trace;
            });
    
             // 更新存储
             storedData.selectedIndex = subplotIndex;
    
            // 返回更新后的 figure 和 dcc.Store 数据
            // 注意: 这里的返回值顺序和 Output() 列表对应
             return [newFig, storedData];
        }
        """,
        Output('subplot-graph', 'figure'),
        Output('selected-subplot-info', 'data'),
        Input('subplot-graph', 'clickData'),
        State('subplot-graph', 'figure'), # 获取当前 figure 状态
        State('selected-subplot-info', 'data') # 获取当前存储状态
    )
    
    if __name__ == '__main__':
        app.run_server(debug=True)
    
    
  2. JavaScript 文件 (assets/custom.js): clientside_callback 中的 JS 代码通常放在这里并引用。对于简单的可以直接写在 Python 字符串里。对于复杂逻辑,外部文件更好管理。

  3. 导出/部署: 这个过程比较复杂。可能需要:

    • 运行 Dash 应用,然后在浏览器中“另存为”完整网页(可能丢失部分动态加载能力)。
    • 使用 dash-renderer 的某些特性或社区工具尝试打包成更独立的版本。
    • 接受它需要一个简单的 HTTP 服务器来提供 HTML 和 JS 文件,即使没有 Python 后端在运行回调。

优点:

  • 利用了 Dash 的事件处理和状态管理机制。
  • 修改图表可以通过更新 figure 对象实现,相对 DOM 操作更符合 Plotly 的模式。
  • 如果能成功打包,交互性可能比纯 JS/CSS 操作更丰富。

缺点:

  • 仍然不是真正的纯静态单 HTML 文件导出 ,通常需要额外的文件(JS, CSS)并且可能依赖于特定的 Dash JS 运行环境。
  • 打包过程可能有挑战,兼容性需要注意。
  • clickData 可能不直接提供子图的标识符,需要从曲线索引或其他信息间接推断,增加了逻辑复杂度。
  • 直接在客户端回调中修改复杂布局(如子图大小、位置)依然困难或不可能,通常是修改数据(颜色、透明度、添加形状等)。

如何选择?

  • 追求纯粹的静态 HTML、仅需简单的视觉高亮: 选择方案一 (JS + CSS) 。这是最符合“静态HTML”要求且相对容易实现的。重点是找到正确的 CSS 选择器和编写简洁的 JS 逻辑。
  • 需要在静态 HTML 中实现聚焦效果(放大/隐藏其他)、不介意复杂度: 选择方案二 (JS 控制显示/隐藏/大小) 。要准备好应对 DOM 结构依赖、布局调整和 Plotly 重绘的挑战。考虑使用 Modal 弹窗可能更健壮。
  • 交互需求复杂、可以接受非纯静态方案或有部署服务器的可能: Dash Client-Side Callback 是一个更强大的选项,但它偏离了严格的静态 HTML 文件目标。适合内部报告或特定部署场景。

根据你的具体需求场景、对“静态”的严格程度以及愿意投入的技术复杂度来选择最合适的方法。对于原始问题中强调的“静态/HTML 文件”输出,方案一 和 方案二(尤其是方案一的视觉高亮)是最直接的解决方案。