Update app.py
Browse files
app.py
CHANGED
|
@@ -183,6 +183,44 @@ def int_to_bin(n: int, d: int) -> str:
|
|
| 183 |
# Even bits → X offsets, odd bits → Y offsets, decreasing magnitudes.
|
| 184 |
def layout_positions(d: int, base: float = 900.0):
|
| 185 |
n = 1 << d # number of nodes = 2^d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
dx, dy = [0.0] * d, [0.0] * d # per-dimension offsets
|
| 187 |
for k in range(d): # dimension k
|
| 188 |
tier = k // 2 # tier 0,1,2,...
|
|
@@ -311,7 +349,8 @@ def make_figure(d: int,
|
|
| 311 |
edge_w: int,
|
| 312 |
path: List[int],
|
| 313 |
scale_base: float,
|
| 314 |
-
subsel: dict | None = None
|
|
|
|
| 315 |
nodes, edges = build_hypercube(d)
|
| 316 |
pts, width, height = layout_positions(d, base=scale_base)
|
| 317 |
pos = {vid: (x, y) for vid, x, y in pts}
|
|
@@ -681,6 +720,16 @@ app.layout = html.Div(
|
|
| 681 |
style={"width": "60px"},
|
| 682 |
),
|
| 683 |
html.Button("Flip", id="btn_flip", n_clicks=0),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
],
|
| 685 |
),
|
| 686 |
|
|
@@ -918,7 +967,8 @@ def update_path(clickData,
|
|
| 918 |
Input("path_store", "data"),
|
| 919 |
Input("mark_negations", "value"),
|
| 920 |
Input("mark_distances", "value"),
|
| 921 |
-
Input("subpath_select_store", "data"),
|
|
|
|
| 922 |
)
|
| 923 |
def render(d, show_labels_vals, path, mark_vals, mark_dist_vals, subsel):
|
| 924 |
labels_vals = show_labels_vals or []
|
|
@@ -941,7 +991,8 @@ def render(d, show_labels_vals, path, mark_vals, mark_dist_vals, subsel):
|
|
| 941 |
edge_w=DEFAULTS["edge_width"],
|
| 942 |
path=path or [],
|
| 943 |
scale_base=float(DEFAULTS["scale"]),
|
| 944 |
-
subsel=subsel or {},
|
|
|
|
| 945 |
)
|
| 946 |
return fig
|
| 947 |
|
|
@@ -1041,12 +1092,35 @@ def style_flip_subpath_button(subsel):
|
|
| 1041 |
|
| 1042 |
if active:
|
| 1043 |
# selection mode ON
|
| 1044 |
-
return {**base, "background": "#
|
| 1045 |
else:
|
| 1046 |
# selection mode OFF
|
| 1047 |
return {**base, "background": "#2563EB"} # blue
|
| 1048 |
|
| 1049 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1050 |
|
| 1051 |
if __name__ == "__main__":
|
| 1052 |
import os
|
|
|
|
| 183 |
# Even bits → X offsets, odd bits → Y offsets, decreasing magnitudes.
|
| 184 |
def layout_positions(d: int, base: float = 900.0):
|
| 185 |
n = 1 << d # number of nodes = 2^d
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if mode == "bipartite":
|
| 189 |
+
# Left: odd parity, Right: even parity (as requested)
|
| 190 |
+
odds = [v for v in range(n) if (bin(v).count("1") % 2) == 1]
|
| 191 |
+
evens = [v for v in range(n) if (bin(v).count("1") % 2) == 0]
|
| 192 |
+
|
| 193 |
+
# Deterministic order (by integer id)
|
| 194 |
+
odds.sort()
|
| 195 |
+
evens.sort()
|
| 196 |
+
|
| 197 |
+
col_gap = max(400.0, base * 0.9) # horizontal distance between columns
|
| 198 |
+
y_gap = max(12.0, base / max(1, (n // 2))) # vertical spacing
|
| 199 |
+
|
| 200 |
+
pts = []
|
| 201 |
+
|
| 202 |
+
# Put odds on the left (x=0)
|
| 203 |
+
for idx, v in enumerate(odds):
|
| 204 |
+
x = 0.0
|
| 205 |
+
y = idx * y_gap
|
| 206 |
+
pts.append((v, x, y))
|
| 207 |
+
|
| 208 |
+
# Put evens on the right (x=col_gap)
|
| 209 |
+
for idx, v in enumerate(evens):
|
| 210 |
+
x = col_gap
|
| 211 |
+
y = idx * y_gap
|
| 212 |
+
pts.append((v, x, y))
|
| 213 |
+
|
| 214 |
+
# normalize to start at (0,0) like your current function
|
| 215 |
+
minx = min(x for _, x, _ in pts)
|
| 216 |
+
miny = min(y for _, _, y in pts)
|
| 217 |
+
pts2 = [(vid, x - minx, y - miny) for vid, x, y in pts]
|
| 218 |
+
|
| 219 |
+
width = col_gap
|
| 220 |
+
height = (len(odds) - 1) * y_gap if odds else 0.0
|
| 221 |
+
return pts2, width, height
|
| 222 |
+
|
| 223 |
+
|
| 224 |
dx, dy = [0.0] * d, [0.0] * d # per-dimension offsets
|
| 225 |
for k in range(d): # dimension k
|
| 226 |
tier = k // 2 # tier 0,1,2,...
|
|
|
|
| 349 |
edge_w: int,
|
| 350 |
path: List[int],
|
| 351 |
scale_base: float,
|
| 352 |
+
subsel: dict | None = None,
|
| 353 |
+
layout_mode: str = "default"):
|
| 354 |
nodes, edges = build_hypercube(d)
|
| 355 |
pts, width, height = layout_positions(d, base=scale_base)
|
| 356 |
pos = {vid: (x, y) for vid, x, y in pts}
|
|
|
|
| 720 |
style={"width": "60px"},
|
| 721 |
),
|
| 722 |
html.Button("Flip", id="btn_flip", n_clicks=0),
|
| 723 |
+
|
| 724 |
+
# Bipartite Layout Button
|
| 725 |
+
html.Button(
|
| 726 |
+
"Bipartite layout",
|
| 727 |
+
id="btn_bipartite_layout",
|
| 728 |
+
n_clicks=0,
|
| 729 |
+
style={"background": "#6B7280", "color": "white"},
|
| 730 |
+
),
|
| 731 |
+
dcc.Store(id="layout_mode_store", data="default"),
|
| 732 |
+
|
| 733 |
],
|
| 734 |
),
|
| 735 |
|
|
|
|
| 967 |
Input("path_store", "data"),
|
| 968 |
Input("mark_negations", "value"),
|
| 969 |
Input("mark_distances", "value"),
|
| 970 |
+
Input("subpath_select_store", "data"),
|
| 971 |
+
Input("layout_mode_store", "data"),
|
| 972 |
)
|
| 973 |
def render(d, show_labels_vals, path, mark_vals, mark_dist_vals, subsel):
|
| 974 |
labels_vals = show_labels_vals or []
|
|
|
|
| 991 |
edge_w=DEFAULTS["edge_width"],
|
| 992 |
path=path or [],
|
| 993 |
scale_base=float(DEFAULTS["scale"]),
|
| 994 |
+
subsel=subsel or {},
|
| 995 |
+
layout_mode=layout_mode or "default", # NEW
|
| 996 |
)
|
| 997 |
return fig
|
| 998 |
|
|
|
|
| 1092 |
|
| 1093 |
if active:
|
| 1094 |
# selection mode ON
|
| 1095 |
+
return {**base, "background": "#2563EB"} # strong blue for selection
|
| 1096 |
else:
|
| 1097 |
# selection mode OFF
|
| 1098 |
return {**base, "background": "#2563EB"} # blue
|
| 1099 |
|
| 1100 |
|
| 1101 |
+
@app.callback(
|
| 1102 |
+
Output("layout_mode_store", "data"),
|
| 1103 |
+
Input("btn_bipartite_layout", "n_clicks"),
|
| 1104 |
+
State("layout_mode_store", "data"),
|
| 1105 |
+
prevent_initial_call=True
|
| 1106 |
+
)
|
| 1107 |
+
def toggle_layout(n, mode):
|
| 1108 |
+
mode = mode or "default"
|
| 1109 |
+
return "bipartite" if mode == "default" else "default"
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
@app.callback(
|
| 1113 |
+
Output("btn_bipartite_layout", "style"),
|
| 1114 |
+
Input("layout_mode_store", "data"),
|
| 1115 |
+
)
|
| 1116 |
+
def style_layout_button(mode):
|
| 1117 |
+
base = {"color": "white", "border": "none", "padding": "6px 12px", "borderRadius": "8px", "cursor": "pointer"}
|
| 1118 |
+
if mode == "bipartite":
|
| 1119 |
+
return {**base, "background": "#059669"} # green
|
| 1120 |
+
return {**base, "background": "#6B7280"} # gray
|
| 1121 |
+
|
| 1122 |
+
|
| 1123 |
+
|
| 1124 |
|
| 1125 |
if __name__ == "__main__":
|
| 1126 |
import os
|