0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

bokehのグラフをHTMLに埋め込んでネスト構造で表示制御する

Last updated at Posted at 2025-07-19

出力されるHTMLファイル

各SectionにBokehグラフA,Bがぶらさがって、タブっぽく表示切替が可能。

image.png

コード

グラフデータ用意

from bokeh.plotting import figure
from bokeh.embed import components
from bokeh.resources import INLINE

# Define section and tab identifiers
sections = ["X", "Y", "Z"]
tabs = ["A", "B"]

# Create figures and store components
contents = {}
for sec in sections:
    for tab in tabs:
        key = f"{sec}{tab}"
        fig = figure(title=f"Figure {key}")
        fig.scatter([1, 2, 3], [ord(sec), ord(tab), ord(sec) + ord(tab)], size=10)
        script, div = components(fig)
        contents[key] = {"script": script, "div": div}

HTML用意

resources = INLINE.render()

html = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Bokeh Dynamic UI</title>
    {resources}
    <style>
        body {{
            font-family: sans-serif;
            margin: 0;
            padding: 0;
        }}
        .header {{
            position: fixed;
            top: 0;
            width: 100%;
            height: 50px;
            background: #333;
            color: white;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            padding: 0 24px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            z-index: 1000;
            box-sizing: border-box;
        }}
        .header-buttons {{
            display: flex;
            gap: 10px;
            flex-wrap: nowrap;
            overflow-x: auto;
        }}
        .section-btn {{
            cursor: pointer;
            padding: 8px 12px;
            background: #666;
            border-radius: 4px;
            font-weight: bold;
            color: white;
            white-space: nowrap;
        }}
        .section-btn.active {{
            background: #fff;
            color: #333;
        }}
        .content {{
            margin-top: 60px;
            padding: 0 20px;
        }}
        .tab-bar {{
            display: flex;
            border-bottom: 1px solid #aaa;
            margin-bottom: 0;
        }}
        .tab-btn {{
            cursor: pointer;
            padding: 10px 20px;
            margin-bottom: -1px;
            background: #f0f0f0;
            border: 1px solid #aaa;
            border-bottom: none;
            border-top-left-radius: 5px;
            border-top-right-radius: 5px;
            font-weight: bold;
        }}
        .tab-btn:not(:last-child) {{
            margin-right: 4px;
        }}
        .tab-btn.active {{
            background: white;
            border-bottom: 1px solid white;
        }}
        .section {{
            display: none;
        }}
        .section.active {{
            display: block;
        }}
        .tab-content {{
            display: none;
            border: 1px solid #aaa;
            padding: 10px;
            border-top: none;
        }}
        .tab-content.active {{
            display: block;
        }}
    </style>
</head>
<body>
    <div class="header">
        <div class="header-buttons">
"""

# Add section buttons
for i, sec in enumerate(sections):
    html += f'<span class="section-btn {"active" if i == 0 else ""}" onclick="showSection(\'{sec}\')">Section {sec}</span>\n'

html += """
        </div>
    </div>
    <div class="content">
        <div class="tab-bar">
"""

# Add tab buttons
for i, tab in enumerate(tabs):
    html += f'<span class="tab-btn {"active" if i == 0 else ""}" onclick="showTab(\'{tab}\')">Tab {tab}</span>\n'

html += "</div>\n"

# Add tab content inside each section
for i, sec in enumerate(sections):
    html += f'<div class="section {sec} {"active" if i == 0 else ""}">\n'
    for j, tab in enumerate(tabs):
        key = f"{sec}{tab}"
        html += f'<div class="tab-content {tab} {"active" if j == 0 else ""}">\n'
        html += contents[key]["div"]
        html += "</div>\n"
    html += "</div>\n"

# Add scripts for all Bokeh figures
for key in contents:
    html += contents[key]["script"] + "\n"

# Add interactive JS
html += f"""
<script>
    let currentTab = '{tabs[0]}';

    function showTab(tab) {{
        currentTab = tab;
        document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
        document.querySelector(`.tab-btn[onclick="showTab('${{tab}}')"]`).classList.add('active');

        document.querySelectorAll('.section.active .tab-content').forEach(div => {{
            div.classList.remove('active');
        }});
        document.querySelector(`.section.active .tab-content.${{tab}}`)?.classList.add('active');
    }}

    function showSection(section) {{
        document.querySelectorAll('.section-btn').forEach(btn => btn.classList.remove('active'));
        document.querySelector(`.section-btn[onclick="showSection('${{section}}')"]`).classList.add('active');

        document.querySelectorAll('.section').forEach(sec => sec.classList.remove('active'));
        const secElem = document.querySelector(`.section.${{section}}`);
        secElem.classList.add('active');

        secElem.querySelectorAll('.tab-content').forEach(div => div.classList.remove('active'));
        const activeTab = secElem.querySelector(`.tab-content.${{currentTab}}`);
        if (activeTab) activeTab.classList.add('active');
    }}
</script>
</div>
</body>
</html>
"""

出力

# Save the final HTML file
output_path = "/".join([output_dir,"bokeh_tab integration.html"])
with open(output_path, "w", encoding="utf-8") as f:
     f.write(html)

print("HTML file has been created.")

追記 (検証中)

初回の表示で、切替直後の表示が一瞬不安定になるところの対策

function triggerBokehResize() {
    if (typeof Bokeh !== 'undefined' && Bokeh.index != null) {
        for (const id in Bokeh.index) {
            if (Bokeh.index.hasOwnProperty(id)) {
                Bokeh.index[id].resize?.();
            }
        }
    }
}

function showTab(tab) {
    currentTab = tab;
    document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
    document.querySelector(`.tab-btn[onclick="showTab('${tab}')"]`).classList.add('active');

    document.querySelectorAll('.section.active .tab-content').forEach(div => {
        div.classList.remove('active');
    });
    const target = document.querySelector(`.section.active .tab-content.${tab}`);
    if (target) {
        target.classList.add('active');
        requestAnimationFrame(triggerBokehResize);
    }
}

function showSection(section) {
    document.querySelectorAll('.section-btn').forEach(btn => btn.classList.remove('active'));
    document.querySelector(`.section-btn[onclick="showSection('${section}')"]`).classList.add('active');

    document.querySelectorAll('.section').forEach(sec => sec.classList.remove('active'));
    const secElem = document.querySelector(`.section.${section}`);
    secElem.classList.add('active');

    secElem.querySelectorAll('.tab-content').forEach(div => div.classList.remove('active'));
    const activeTab = secElem.querySelector(`.tab-content.${currentTab}`);
    if (activeTab) {
        activeTab.classList.add('active');
        requestAnimationFrame(triggerBokehResize);
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?