Skip to content

Commit fc0ff4b

Browse files
feat(plotly): implement sequence-logo-basic (#4615)
## Implementation: `sequence-logo-basic` - plotly Implements the **plotly** version of `sequence-logo-basic`. **File:** `plots/sequence-logo-basic/implementations/plotly.py` **Parent Issue:** #4421 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/22780525003)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 29f7ab1 commit fc0ff4b

2 files changed

Lines changed: 473 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
""" pyplots.ai
2+
sequence-logo-basic: Sequence Logo for Motif Visualization
3+
Library: plotly 6.6.0 | Python 3.14.3
4+
Quality: 86/100 | Created: 2026-03-06
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data - transcription factor binding site motif (10-position DNA)
12+
# Position weight matrix: each row is [A, C, G, T] frequencies
13+
pwm = np.array(
14+
[
15+
[0.05, 0.80, 0.05, 0.10], # pos 1: C dominant
16+
[0.10, 0.15, 0.10, 0.65], # pos 2: T dominant
17+
[0.02, 0.96, 0.01, 0.01], # pos 3: C highly conserved (~1.8 bits)
18+
[0.25, 0.25, 0.25, 0.25], # pos 4: uniform (0 bits)
19+
[0.70, 0.05, 0.15, 0.10], # pos 5: A dominant
20+
[0.10, 0.10, 0.70, 0.10], # pos 6: G dominant
21+
[0.001, 0.001, 0.001, 0.997], # pos 7: T near-perfect conservation (~1.97 bits)
22+
[0.60, 0.15, 0.15, 0.10], # pos 8: A dominant
23+
[0.10, 0.10, 0.65, 0.15], # pos 9: G dominant
24+
[0.15, 0.55, 0.10, 0.20], # pos 10: C dominant
25+
]
26+
)
27+
28+
letters = ["A", "C", "G", "T"]
29+
# Standard DNA sequence logo colors: A=green, C=blue, G=orange, T=red
30+
# Using colorblind-safe Wong palette variants
31+
colors = {"A": "#009E73", "C": "#0072B2", "G": "#E69F00", "T": "#D55E00"}
32+
n_positions = len(pwm)
33+
34+
# Information content: IC = 2 + sum(f * log2(f)) for DNA (max 2 bits)
35+
info_content = np.zeros(n_positions)
36+
for i in range(n_positions):
37+
entropy = sum(f * np.log2(f) for f in pwm[i] if f > 0)
38+
info_content[i] = max(0, 2.0 + entropy)
39+
40+
# Letter heights = frequency * information content at each position
41+
letter_heights = pwm * info_content[:, np.newaxis]
42+
43+
# SVG path data for letters (simplified block-style glyphs within 0-1 x 0-1 box)
44+
letter_paths = {
45+
"A": "M 0.5 0 L 0.05 1 L 0.25 1 L 0.35 0.7 L 0.65 0.7 L 0.75 1 L 0.95 1 L 0.5 0 Z M 0.4 0.52 L 0.6 0.52 L 0.55 0.38 L 0.45 0.38 Z",
46+
"C": "M 0.85 0.2 C 0.65 -0.05 0.2 0 0.1 0.3 C 0 0.6 0.15 0.95 0.5 1 C 0.7 1.02 0.85 0.9 0.88 0.8 L 0.68 0.7 C 0.6 0.82 0.5 0.82 0.4 0.78 C 0.28 0.7 0.25 0.5 0.3 0.35 C 0.35 0.2 0.5 0.15 0.6 0.18 C 0.68 0.2 0.72 0.28 0.75 0.32 Z",
47+
"G": "M 0.85 0.2 C 0.65 -0.05 0.2 0 0.1 0.3 C 0 0.6 0.15 0.95 0.5 1 C 0.7 1.02 0.85 0.9 0.88 0.8 L 0.68 0.7 C 0.6 0.82 0.5 0.82 0.4 0.78 C 0.28 0.7 0.25 0.5 0.3 0.35 C 0.35 0.2 0.5 0.15 0.6 0.18 C 0.68 0.2 0.72 0.28 0.75 0.32 L 0.85 0.2 Z M 0.55 0.45 L 0.85 0.45 L 0.85 0.55 L 0.55 0.55 Z",
48+
"T": "M 0.05 0 L 0.05 0.18 L 0.38 0.18 L 0.38 1 L 0.62 1 L 0.62 0.18 L 0.95 0.18 L 0.95 0 Z",
49+
}
50+
51+
# Plot - using Plotly shapes for stretched letter glyphs
52+
fig = go.Figure()
53+
bar_width = 0.38 # half-width for shapes
54+
55+
# Build letter shapes at each position
56+
for pos in range(n_positions):
57+
heights = letter_heights[pos]
58+
sorted_indices = np.argsort(heights)
59+
y_bottom = 0
60+
61+
for idx in sorted_indices:
62+
h = heights[idx]
63+
if h < 0.005:
64+
continue
65+
letter = letters[idx]
66+
67+
# Add invisible bar for hover interaction
68+
fig.add_trace(
69+
go.Bar(
70+
x=[pos + 1],
71+
y=[h],
72+
base=y_bottom,
73+
width=bar_width * 2,
74+
marker={"color": "rgba(0,0,0,0)", "line": {"width": 0}},
75+
showlegend=False,
76+
hovertemplate=(
77+
f"<b>Position {pos + 1}</b><br>"
78+
f"Nucleotide: {letter}<br>"
79+
f"Frequency: {pwm[pos][idx]:.0%}<br>"
80+
f"Height: {h:.3f} bits"
81+
f"<extra></extra>"
82+
),
83+
)
84+
)
85+
86+
# Transform SVG path from 0-1 space to data coordinates (inline)
87+
tokens = letter_paths[letter].split()
88+
path_parts = []
89+
ti = 0
90+
while ti < len(tokens):
91+
cmd = tokens[ti]
92+
if cmd in ("M", "L", "Z"):
93+
path_parts.append(cmd)
94+
if cmd != "Z":
95+
px = float(tokens[ti + 1])
96+
py = float(tokens[ti + 2])
97+
path_parts.append(str((pos + 1) - bar_width + px * 2 * bar_width))
98+
path_parts.append(str(y_bottom + py * h))
99+
ti += 3
100+
else:
101+
ti += 1
102+
elif cmd == "C":
103+
path_parts.append(cmd)
104+
for j in range(3):
105+
px = float(tokens[ti + 1 + j * 2])
106+
py = float(tokens[ti + 2 + j * 2])
107+
path_parts.append(str((pos + 1) - bar_width + px * 2 * bar_width))
108+
path_parts.append(str(y_bottom + py * h))
109+
ti += 7
110+
else:
111+
ti += 1
112+
path_data = " ".join(path_parts)
113+
114+
fig.add_shape(
115+
type="path",
116+
path=path_data,
117+
fillcolor=colors[letter],
118+
line={"width": 0.5, "color": colors[letter]},
119+
layer="above",
120+
xref="x",
121+
yref="y",
122+
)
123+
124+
y_bottom += h
125+
126+
# Legend entries
127+
for letter in letters:
128+
fig.add_trace(
129+
go.Scatter(
130+
x=[None],
131+
y=[None],
132+
mode="markers",
133+
marker={"size": 18, "color": colors[letter], "symbol": "square"},
134+
name=f" {letter} ",
135+
showlegend=True,
136+
)
137+
)
138+
139+
# Style
140+
fig.update_layout(
141+
title={
142+
"text": "sequence-logo-basic · plotly · pyplots.ai",
143+
"font": {"size": 28, "family": "Arial, Helvetica, sans-serif", "color": "#1a1a2e"},
144+
"x": 0.5,
145+
"y": 0.96,
146+
},
147+
xaxis={
148+
"title": {
149+
"text": "Position",
150+
"font": {"size": 22, "color": "#1a1a2e", "family": "Arial, sans-serif"},
151+
"standoff": 12,
152+
},
153+
"tickfont": {"size": 18, "color": "#4a4a68", "family": "Arial, sans-serif"},
154+
"tickvals": list(range(1, n_positions + 1)),
155+
"showline": True,
156+
"linewidth": 2,
157+
"linecolor": "#1a1a2e",
158+
"mirror": False,
159+
"showgrid": False,
160+
"zeroline": False,
161+
"ticks": "outside",
162+
"ticklen": 8,
163+
"tickwidth": 1.5,
164+
"tickcolor": "#4a4a68",
165+
},
166+
yaxis={
167+
"title": {
168+
"text": "Information content (bits)",
169+
"font": {"size": 22, "color": "#1a1a2e", "family": "Arial, sans-serif"},
170+
"standoff": 10,
171+
},
172+
"tickfont": {"size": 18, "color": "#4a4a68", "family": "Arial, sans-serif"},
173+
"range": [0, 2.15],
174+
"showline": True,
175+
"linewidth": 2,
176+
"linecolor": "#1a1a2e",
177+
"mirror": False,
178+
"gridwidth": 0.5,
179+
"gridcolor": "rgba(100,100,140,0.08)",
180+
"griddash": "dot",
181+
"zeroline": True,
182+
"zerolinewidth": 2,
183+
"zerolinecolor": "#1a1a2e",
184+
"ticks": "outside",
185+
"ticklen": 8,
186+
"tickwidth": 1.5,
187+
"tickcolor": "#4a4a68",
188+
"dtick": 0.5,
189+
},
190+
template="plotly_white",
191+
barmode="overlay",
192+
bargap=0,
193+
plot_bgcolor="white",
194+
paper_bgcolor="white",
195+
legend={
196+
"font": {"size": 20, "family": "Arial Black, Impact, sans-serif"},
197+
"orientation": "h",
198+
"yanchor": "bottom",
199+
"y": 1.04,
200+
"xanchor": "center",
201+
"x": 0.5,
202+
"bgcolor": "rgba(0,0,0,0)",
203+
"tracegroupgap": 20,
204+
},
205+
margin={"l": 90, "r": 50, "t": 120, "b": 70},
206+
hoverlabel={"bgcolor": "white", "bordercolor": "#4a4a68", "font": {"size": 15, "family": "Arial, sans-serif"}},
207+
)
208+
209+
# Annotate highly conserved positions with larger, more prominent labels
210+
conserved_positions = [2, 6] # positions 3 and 7 (0-indexed)
211+
for pos_idx in conserved_positions:
212+
ic_val = info_content[pos_idx]
213+
fig.add_annotation(
214+
x=pos_idx + 1,
215+
y=ic_val + 0.08,
216+
text=f"▼ {ic_val:.2f} bits",
217+
font={
218+
"size": 16,
219+
"color": "#1a1a2e",
220+
"family": "Arial, sans-serif",
221+
"weight": "bold" if ic_val > 1.9 else "normal",
222+
},
223+
showarrow=False,
224+
yanchor="bottom",
225+
xanchor="center",
226+
)
227+
228+
# Add subtle annotation for the zero-information position
229+
fig.add_annotation(
230+
x=4,
231+
y=-0.08,
232+
text="no signal",
233+
font={"size": 13, "color": "#999999", "family": "Arial, sans-serif"},
234+
showarrow=False,
235+
yanchor="top",
236+
xanchor="center",
237+
)
238+
239+
# Save
240+
fig.write_html("plot.html")
241+
fig.write_image("plot.png", width=1600, height=900, scale=3)

0 commit comments

Comments
 (0)