出力されるHTML
plotlyでは凡例クリック時に非表示化する機能があります。
このスクリプトでは、完全に非表示するのではなく、以下のように透明度を操作します。
コード
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>
"""