1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTMLに埋め込んだplotlyグラフを凡例クリック時にグレーアウトさせる

Last updated at Posted at 2025-07-29

出力されるHTML

plotlyでは凡例クリック時に非表示化する機能があります。
このスクリプトでは、完全に非表示するのではなく、以下のように透明度を操作します。
image.png

コード

plotly_legendclickイベントをカスタム実装で上書きするため、以下の標準機能は失われます。
・凡例ダブルクリックで1系列のみをアクティブ化
・もう一回ダブルクリックすると全てアクティブ化

→ ダブルクリックで1系列のみをアクティブ化する機能を追加しました。

import plotly.express as px

# サンプルデータでPlotly Express図を作成
df = px.data.iris()
fig = px.scatter(
    df,
    x="sepal_width",
    y="sepal_length",
    color="species",
    title="Iris Plot"
)

# HTMLからdivのみ抽出
html_div = fig.to_html(
    full_html=False,
    include_plotlyjs=False
)

# plotlyjs取得。versionは適宜変更してください。
plotlyjs = "https://cdn.plot.ly/plotly-2.32.0.min.js"

# 非アクティブ時の透明度
opacity = 0.3

# クリックとダブルクリックの判定時間(ms)
delay = 250

# カスタムHTML
html = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Plotly Express Legend Custom Clicks</title>
    <script src="{plotlyjs}"></script>
</head>
<body>

{html_div}

<script>
    var gd = document.querySelectorAll(".js-plotly-plot")[0];

    var traceNames = gd.data.map(d => d.name);
    var activeStates = {{}};
    traceNames.forEach(name => activeStates[name] = true);

    // click/doubleclick判定用変数
    var clickTimer = null;
    var delay = {delay};  // クリックとダブルクリックの判定時間(ms)

    gd.on('plotly_legendclick', function(eventData) {{
        if (clickTimer) {{
            // ダブルクリックと判定
            clearTimeout(clickTimer);
            clickTimer = null;

            const clickedName = gd.data[eventData.curveNumber].name;

            const newOpacity = gd.data.map(trace =>
                trace.name === clickedName ? 1.0 : {opacity}
            );

            gd.data.forEach(trace => {{
                activeStates[trace.name] = (trace.name === clickedName);
            }});

            Plotly.restyle(gd, {{opacity: newOpacity}});
        }} else {{
            // シングルクリックかもしれない → 遅延実行
            clickTimer = setTimeout(function() {{
                const traceIndex = eventData.curveNumber;
                const clickedName = gd.data[traceIndex].name;
                activeStates[clickedName] = !activeStates[clickedName];

                const newOpacity = gd.data.map(trace =>
                    activeStates[trace.name] ? 1.0 : {opacity}
                );

                Plotly.restyle(gd, {{opacity: newOpacity}});
                clickTimer = null;
            }}, delay);
        }}
        return false;  // 標準動作キャンセル
    }});
</script>

</body>
</html>
"""

# ファイルに保存
output_path = "plotly_legend_grayout.html"
with open(output_path, "w", encoding="utf-8") as f:
     f.write(html)

最後に

facet構成のプロットではうまく動作しないと思います。
ほかに何かあれば教えてください。

以下はまだ動作未確認

右クリック対応版

# Optimized right-click JS
const plot = document.querySelector('div.js-plotly-plot');

let lastIndex = null;
let isIsolated = false;

plot.addEventListener('contextmenu', function(event) {
  event.preventDefault();
});

plot.addEventListener('pointerdown', function(event) {
  if (event.button !== 2) return;

  const legendItems = plot.getElementsByClassName('legendtoggle');

  Array.from(legendItems).forEach((legendItem, index) => {
    if (legendItem.contains(event.target)) {
      event.preventDefault();
      event.stopPropagation();

      const nTraces = plot.data.length;

      if (isIsolated && lastIndex === index) {
        const opacities = Array(nTraces).fill(1);
        const zindexes = Array(nTraces).fill(1);
        Plotly.restyle(plot, {opacity: opacities, zindex: zindexes});
        isIsolated = false;
        lastIndex = null;
      } else {
        const opacities = Array(nTraces).fill(0.2);
        const zindexes = Array(nTraces).fill(0);
        opacities[index] = 1;
        zindexes[index] = 1;
        Plotly.restyle(plot, {opacity: opacities, zindex: zindexes});
        isIsolated = true;
        lastIndex = index;
      }
    }
  });
});

# Optimized left-click JS
plot.on('plotly_legendclick', function(eventData) {
  const index = eventData.curveNumber;
  const currentOpacity = plot.data[index].opacity ?? 1;
  const newOpacity = currentOpacity === 1 ? 0.2 : 1;
  const newZ = currentOpacity === 1 ? 0 : 1;

  Plotly.restyle(plot, {
    opacity: [newOpacity],
    zindex: [newZ]
  }, [index]);

  return false;
});

facet対応版

import plotly.express as px
import pandas as pd
import numpy as np
import json

# データ作成
np.random.seed(42)
n = 120
df = pd.DataFrame({
    "x": np.random.rand(n) * 10,
    "y": np.random.rand(n) * 10,
    "category": np.random.choice(["A", "B", "C"], size=n),
    "facet_row": np.random.choice(["Top", "Bottom"], size=n),
    "facet_col": np.random.choice(["Left", "Right"], size=n),
})

# Plotly Expressでfacet付きグラフ作成
fig = px.scatter(
    df, x="x", y="y", color="category",
    facet_row="facet_row", facet_col="facet_col",
    title="Legend Click: Toggle, DoubleClick: Highlight Only"
)

# JSON変換対応(ndarray → list)
def convert_ndarray(obj):
    if isinstance(obj, np.ndarray):
        return obj.tolist()
    raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")

# JSON化
fig_json = fig.to_plotly_json()
fig_data = json.dumps(fig_json["data"], default=convert_ndarray)
fig_layout = json.dumps(fig_json["layout"], default=convert_ndarray)

# Plotlyjs取得
plotlyjs = "https://cdn.plot.ly/plotly-2.32.0.min.js"

# 非アクティブ時の透明度
opacity = 0.3

# HTML構成
html_facet = f"""
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Legend Click and DoubleClick Behavior</title>
  <script src="{plotlyjs}"></script>
</head>
<body>
  <div id="plot" style="width:90vw; height:90vh;"></div>
  <script>
    const data = {fig_data};
    const layout = {fig_layout};

    Plotly.newPlot('plot', data, layout).then(gd => {{

      const activeStates = {{}};
      gd.data.forEach(trace => {{
        if (!(trace.name in activeStates)) {{
          activeStates[trace.name] = true;
        }}
      }});

      // シングルクリック:トグル表示(グレーアウト)
      gd.on('plotly_legendclick', function(eventData) {{
        const clickedName = gd.data[eventData.curveNumber].name;
        activeStates[clickedName] = !activeStates[clickedName];

        const newOpacities = gd.data.map(trace =>
          activeStates[trace.name] ? 1.0 : {opacity}
        );

        Plotly.restyle(gd, {{opacity: newOpacities}});
        return false;
      }});

      // ダブルクリック:該当系列のみ強調表示(他はグレーアウト)
      gd.on('plotly_legenddoubleclick', function(eventData) {{
        const clickedName = gd.data[eventData.curveNumber].name;

        const newOpacities = gd.data.map(trace =>
          trace.name === clickedName ? 1.0 : {opacity}
        );

        gd.data.forEach(trace => {{
          activeStates[trace.name] = (trace.name === clickedName);
        }});

        Plotly.restyle(gd, {{opacity: newOpacities}});
        return false;
      }});
    }});
  </script>
</body>
</html>
"""
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?