Update app.py
Browse files
app.py
CHANGED
|
@@ -728,12 +728,16 @@ app.layout = html.Div(
|
|
| 728 |
),
|
| 729 |
]),
|
| 730 |
|
| 731 |
-
# --- Subpath flip controls ---
|
| 732 |
html.Div(
|
| 733 |
style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"},
|
| 734 |
children=[
|
| 735 |
html.Button("Flip subpath", id="btn_flip_subpath", n_clicks=0,
|
| 736 |
style={"background": "#2563EB", "color": "white"}),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
dcc.Input(
|
| 738 |
id="subpath_dim",
|
| 739 |
type="number",
|
|
@@ -750,6 +754,12 @@ app.layout = html.Div(
|
|
| 750 |
id="subpath_select_store",
|
| 751 |
data={"active": False, "start_idx": None, "end_idx": None}
|
| 752 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
|
| 754 |
html.Div(), # empty cell just to keep the grid tidy
|
| 755 |
],
|
|
@@ -865,7 +875,8 @@ def stats(d, path):
|
|
| 865 |
|
| 866 |
@app.callback(
|
| 867 |
Output("path_store", "data"),
|
| 868 |
-
Output("subpath_select_store", "data"),
|
|
|
|
| 869 |
Input("fig", "clickData"),
|
| 870 |
Input("btn_clear", "n_clicks"),
|
| 871 |
Input("btn_longest_cib", "n_clicks"),
|
|
@@ -874,15 +885,17 @@ def stats(d, path):
|
|
| 874 |
Input("btn_set", "n_clicks"),
|
| 875 |
Input("btn_swap", "n_clicks"),
|
| 876 |
Input("btn_flip", "n_clicks"),
|
| 877 |
-
Input("btn_flip_subpath", "n_clicks"),
|
|
|
|
| 878 |
State("path_store", "data"),
|
| 879 |
State("manual_path", "value"),
|
| 880 |
State("dim", "value"),
|
| 881 |
State("swap_i", "value"),
|
| 882 |
State("swap_j", "value"),
|
| 883 |
State("flip_k", "value"),
|
| 884 |
-
State("subpath_select_store", "data"),
|
| 885 |
-
State("subpath_dim", "value"),
|
|
|
|
| 886 |
prevent_initial_call=True
|
| 887 |
)
|
| 888 |
def update_path(clickData,
|
|
@@ -894,6 +907,7 @@ def update_path(clickData,
|
|
| 894 |
n_swap,
|
| 895 |
n_flip,
|
| 896 |
n_flip_subpath,
|
|
|
|
| 897 |
path,
|
| 898 |
manual_text,
|
| 899 |
d,
|
|
@@ -901,7 +915,8 @@ def update_path(clickData,
|
|
| 901 |
swap_j,
|
| 902 |
flip_k,
|
| 903 |
subsel,
|
| 904 |
-
subpath_dim
|
|
|
|
| 905 |
|
| 906 |
trigger = ctx.triggered_id
|
| 907 |
path = path or []
|
|
@@ -909,28 +924,30 @@ def update_path(clickData,
|
|
| 909 |
|
| 910 |
# default selection store
|
| 911 |
subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
|
|
|
|
|
|
|
| 912 |
|
| 913 |
# 1) Clear
|
| 914 |
if trigger == "btn_clear":
|
| 915 |
-
return [], {"active": False, "start_idx": None, "end_idx": None}
|
| 916 |
|
| 917 |
# 2a) Longest CIB
|
| 918 |
if trigger == "btn_longest_cib":
|
| 919 |
-
return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}
|
| 920 |
|
| 921 |
# 2b) Longest Symmetric CIB
|
| 922 |
if trigger == "btn_longest_sym_cib":
|
| 923 |
-
return longest_sym_cib(d), {"active": False, "start_idx": None, "end_idx": None}
|
| 924 |
|
| 925 |
|
| 926 |
# 2c) Shorter CIB
|
| 927 |
if trigger == "btn_shorter_cib":
|
| 928 |
-
return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}
|
| 929 |
|
| 930 |
# 3) Manual set path
|
| 931 |
if trigger == "btn_set":
|
| 932 |
newp = parse_path(manual_text or "", d)
|
| 933 |
-
return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}
|
| 934 |
|
| 935 |
# 4) Swap two dimensions
|
| 936 |
if trigger == "btn_swap":
|
|
@@ -938,22 +955,22 @@ def update_path(clickData,
|
|
| 938 |
i = int(swap_i) if swap_i is not None else None
|
| 939 |
j = int(swap_j) if swap_j is not None else None
|
| 940 |
except (TypeError, ValueError):
|
| 941 |
-
return path, subsel
|
| 942 |
if i is None or j is None:
|
| 943 |
return path, subsel
|
| 944 |
if not (0 <= i < d and 0 <= j < d):
|
| 945 |
-
return path, subsel
|
| 946 |
-
return swap_dims_path(path, d, i, j), subsel
|
| 947 |
|
| 948 |
# 5) Flip one dimension (whole path)
|
| 949 |
if trigger == "btn_flip":
|
| 950 |
try:
|
| 951 |
k = int(flip_k) if flip_k is not None else None
|
| 952 |
except (TypeError, ValueError):
|
| 953 |
-
return path, subsel
|
| 954 |
if k is None or not (0 <= k < d):
|
| 955 |
-
return path, subsel
|
| 956 |
-
return flip_dim_path(path, d, k), subsel
|
| 957 |
|
| 958 |
# 6) The new two-click button:
|
| 959 |
# First click: enter selection mode.
|
|
@@ -962,7 +979,7 @@ def update_path(clickData,
|
|
| 962 |
active = bool(subsel.get("active"))
|
| 963 |
if not active:
|
| 964 |
# enter selection mode
|
| 965 |
-
return path, {"active": True, "start_idx": None, "end_idx": None}
|
| 966 |
|
| 967 |
# active == True, so this is the "second click": apply
|
| 968 |
try:
|
|
@@ -971,13 +988,13 @@ def update_path(clickData,
|
|
| 971 |
i = None
|
| 972 |
if i is None or not (0 <= i < d):
|
| 973 |
# invalid dimension, just exit selection mode
|
| 974 |
-
return path, {"active": False, "start_idx": None, "end_idx": None}
|
| 975 |
|
| 976 |
s = subsel.get("start_idx")
|
| 977 |
e = subsel.get("end_idx")
|
| 978 |
if s is None or e is None or e <= s:
|
| 979 |
# nothing meaningful selected, exit
|
| 980 |
-
return path, {"active": False, "start_idx": None, "end_idx": None}
|
| 981 |
|
| 982 |
# subpath is path[s:e+1] = (v1,...,vk)
|
| 983 |
# desired: keep prefix (..., v1), keep suffix (vk, ...),
|
|
@@ -987,74 +1004,138 @@ def update_path(clickData,
|
|
| 987 |
rest = path[e:] # starts at vk
|
| 988 |
new_path = head + flipped_subpath + rest
|
| 989 |
|
| 990 |
-
return new_path, {"active": False, "start_idx": None, "end_idx": None}
|
| 991 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 992 |
|
| 993 |
-
|
|
|
|
| 994 |
if trigger == "fig" and clickData and clickData.get("points"):
|
| 995 |
p = clickData["points"][0]
|
| 996 |
cd = p.get("customdata")
|
| 997 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
# If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
|
| 999 |
if subsel.get("active") and isinstance(cd, (int, float)):
|
| 1000 |
vid = int(cd)
|
| 1001 |
idx = index_in_path(path, vid)
|
| 1002 |
if idx is None:
|
| 1003 |
-
return path, subsel
|
| 1004 |
|
| 1005 |
s = subsel.get("start_idx")
|
| 1006 |
e = subsel.get("end_idx")
|
| 1007 |
|
| 1008 |
# first selected vertex
|
| 1009 |
if s is None:
|
| 1010 |
-
return path, {"active": True, "start_idx": idx, "end_idx": idx}
|
| 1011 |
|
| 1012 |
# enforce consecutive forward selection along the path
|
| 1013 |
# user must click idx == e+1 to extend
|
| 1014 |
if e is None:
|
| 1015 |
e = s
|
| 1016 |
if idx == e + 1:
|
| 1017 |
-
return path, {"active": True, "start_idx": s, "end_idx": idx}
|
| 1018 |
|
| 1019 |
# allow shrinking by clicking the current end again
|
| 1020 |
if idx == e:
|
| 1021 |
# pop last selected (shrink by 1) if possible
|
| 1022 |
new_e = e - 1 if e > s else s
|
| 1023 |
-
return path, {"active": True, "start_idx": s, "end_idx": new_e}
|
| 1024 |
|
| 1025 |
# otherwise ignore
|
| 1026 |
-
return path, subsel
|
| 1027 |
|
| 1028 |
# Normal mode: your existing click-to-build-path behavior
|
| 1029 |
if isinstance(cd, (int, float)):
|
| 1030 |
vid = int(cd)
|
| 1031 |
|
| 1032 |
if not path:
|
| 1033 |
-
return [vid], subsel
|
| 1034 |
|
| 1035 |
if vid == path[-1]:
|
| 1036 |
-
return path[:-1], subsel
|
| 1037 |
|
| 1038 |
if len(path) >= 2 and vid == path[-2]:
|
| 1039 |
-
return path[:-1], subsel
|
| 1040 |
|
| 1041 |
if hamming_dist(vid, path[-1]) == 1:
|
| 1042 |
-
return path + [vid], subsel
|
| 1043 |
|
| 1044 |
-
return [vid], subsel
|
| 1045 |
|
| 1046 |
if isinstance(cd, (list, tuple)) and len(cd) == 2:
|
| 1047 |
u, v = int(cd[0]), int(cd[1])
|
| 1048 |
if not path:
|
| 1049 |
-
return [u, v], subsel
|
| 1050 |
last = path[-1]
|
| 1051 |
if last == u:
|
| 1052 |
-
return path + [v], subsel
|
| 1053 |
if last == v:
|
| 1054 |
-
return path + [u], subsel
|
| 1055 |
-
return [u, v], subsel
|
| 1056 |
|
| 1057 |
-
return path, subsel
|
| 1058 |
|
| 1059 |
|
| 1060 |
@app.callback(
|
|
@@ -1251,8 +1332,24 @@ def mark_neighbors_label(d, path, mark_dist_vals):
|
|
| 1251 |
labelStyle={"display": "inline-block", "background-color": "yellow"},
|
| 1252 |
)
|
| 1253 |
|
| 1254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
|
|
|
|
|
|
|
| 1256 |
if __name__ == "__main__":
|
| 1257 |
import os
|
| 1258 |
port = int(os.environ.get("PORT", "7860")) # HF uses 7860
|
|
|
|
| 728 |
),
|
| 729 |
]),
|
| 730 |
|
| 731 |
+
# --- Subpath flip controls + Switch dimensions ---
|
| 732 |
html.Div(
|
| 733 |
style={"display": "flex", "gap": "8px", "alignItems": "center", "marginBottom": "8px"},
|
| 734 |
children=[
|
| 735 |
html.Button("Flip subpath", id="btn_flip_subpath", n_clicks=0,
|
| 736 |
style={"background": "#2563EB", "color": "white"}),
|
| 737 |
+
|
| 738 |
+
html.Button("Switch dimensions", id="btn_switch_dims", n_clicks=0,
|
| 739 |
+
style={"background": "#6B7280", "color": "white"}),
|
| 740 |
+
|
| 741 |
dcc.Input(
|
| 742 |
id="subpath_dim",
|
| 743 |
type="number",
|
|
|
|
| 754 |
id="subpath_select_store",
|
| 755 |
data={"active": False, "start_idx": None, "end_idx": None}
|
| 756 |
),
|
| 757 |
+
|
| 758 |
+
dcc.Store(
|
| 759 |
+
id="switch_dims_store",
|
| 760 |
+
data={"active": False, "start_idx": None, "end_idx": None}
|
| 761 |
+
),
|
| 762 |
+
|
| 763 |
|
| 764 |
html.Div(), # empty cell just to keep the grid tidy
|
| 765 |
],
|
|
|
|
| 875 |
|
| 876 |
@app.callback(
|
| 877 |
Output("path_store", "data"),
|
| 878 |
+
Output("subpath_select_store", "data"),
|
| 879 |
+
Output("switch_dims_store", "data"),
|
| 880 |
Input("fig", "clickData"),
|
| 881 |
Input("btn_clear", "n_clicks"),
|
| 882 |
Input("btn_longest_cib", "n_clicks"),
|
|
|
|
| 885 |
Input("btn_set", "n_clicks"),
|
| 886 |
Input("btn_swap", "n_clicks"),
|
| 887 |
Input("btn_flip", "n_clicks"),
|
| 888 |
+
Input("btn_flip_subpath", "n_clicks"),
|
| 889 |
+
Input("btn_switch_dims", "n_clicks"),
|
| 890 |
State("path_store", "data"),
|
| 891 |
State("manual_path", "value"),
|
| 892 |
State("dim", "value"),
|
| 893 |
State("swap_i", "value"),
|
| 894 |
State("swap_j", "value"),
|
| 895 |
State("flip_k", "value"),
|
| 896 |
+
State("subpath_select_store", "data"),
|
| 897 |
+
State("subpath_dim", "value"),
|
| 898 |
+
State("switch_dims_store", "data"),
|
| 899 |
prevent_initial_call=True
|
| 900 |
)
|
| 901 |
def update_path(clickData,
|
|
|
|
| 907 |
n_swap,
|
| 908 |
n_flip,
|
| 909 |
n_flip_subpath,
|
| 910 |
+
n_switch_dims,
|
| 911 |
path,
|
| 912 |
manual_text,
|
| 913 |
d,
|
|
|
|
| 915 |
swap_j,
|
| 916 |
flip_k,
|
| 917 |
subsel,
|
| 918 |
+
subpath_dim,
|
| 919 |
+
switchsel):
|
| 920 |
|
| 921 |
trigger = ctx.triggered_id
|
| 922 |
path = path or []
|
|
|
|
| 924 |
|
| 925 |
# default selection store
|
| 926 |
subsel = subsel or {"active": False, "start_idx": None, "end_idx": None}
|
| 927 |
+
switchsel = switchsel or {"active": False, "start_idx": None, "end_idx": None}
|
| 928 |
+
|
| 929 |
|
| 930 |
# 1) Clear
|
| 931 |
if trigger == "btn_clear":
|
| 932 |
+
return [], {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 933 |
|
| 934 |
# 2a) Longest CIB
|
| 935 |
if trigger == "btn_longest_cib":
|
| 936 |
+
return longest_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 937 |
|
| 938 |
# 2b) Longest Symmetric CIB
|
| 939 |
if trigger == "btn_longest_sym_cib":
|
| 940 |
+
return longest_sym_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 941 |
|
| 942 |
|
| 943 |
# 2c) Shorter CIB
|
| 944 |
if trigger == "btn_shorter_cib":
|
| 945 |
+
return shorter_cib(d), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 946 |
|
| 947 |
# 3) Manual set path
|
| 948 |
if trigger == "btn_set":
|
| 949 |
newp = parse_path(manual_text or "", d)
|
| 950 |
+
return (newp if newp else path), {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 951 |
|
| 952 |
# 4) Swap two dimensions
|
| 953 |
if trigger == "btn_swap":
|
|
|
|
| 955 |
i = int(swap_i) if swap_i is not None else None
|
| 956 |
j = int(swap_j) if swap_j is not None else None
|
| 957 |
except (TypeError, ValueError):
|
| 958 |
+
return path, subsel, switchsel
|
| 959 |
if i is None or j is None:
|
| 960 |
return path, subsel
|
| 961 |
if not (0 <= i < d and 0 <= j < d):
|
| 962 |
+
return path, subsel, switchsel
|
| 963 |
+
return swap_dims_path(path, d, i, j), subsel, switchsel
|
| 964 |
|
| 965 |
# 5) Flip one dimension (whole path)
|
| 966 |
if trigger == "btn_flip":
|
| 967 |
try:
|
| 968 |
k = int(flip_k) if flip_k is not None else None
|
| 969 |
except (TypeError, ValueError):
|
| 970 |
+
return path, subsel, switchsel
|
| 971 |
if k is None or not (0 <= k < d):
|
| 972 |
+
return path, subsel, switchsel
|
| 973 |
+
return flip_dim_path(path, d, k), subsel, switchsel
|
| 974 |
|
| 975 |
# 6) The new two-click button:
|
| 976 |
# First click: enter selection mode.
|
|
|
|
| 979 |
active = bool(subsel.get("active"))
|
| 980 |
if not active:
|
| 981 |
# enter selection mode
|
| 982 |
+
return path, {"active": True, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 983 |
|
| 984 |
# active == True, so this is the "second click": apply
|
| 985 |
try:
|
|
|
|
| 988 |
i = None
|
| 989 |
if i is None or not (0 <= i < d):
|
| 990 |
# invalid dimension, just exit selection mode
|
| 991 |
+
return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 992 |
|
| 993 |
s = subsel.get("start_idx")
|
| 994 |
e = subsel.get("end_idx")
|
| 995 |
if s is None or e is None or e <= s:
|
| 996 |
# nothing meaningful selected, exit
|
| 997 |
+
return path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 998 |
|
| 999 |
# subpath is path[s:e+1] = (v1,...,vk)
|
| 1000 |
# desired: keep prefix (..., v1), keep suffix (vk, ...),
|
|
|
|
| 1004 |
rest = path[e:] # starts at vk
|
| 1005 |
new_path = head + flipped_subpath + rest
|
| 1006 |
|
| 1007 |
+
return new_path, {"active": False, "start_idx": None, "end_idx": None}, {"active": False, "start_idx": None, "end_idx": None}
|
| 1008 |
|
| 1009 |
+
# 7) Switch dimensions mode toggle
|
| 1010 |
+
if trigger == "btn_switch_dims":
|
| 1011 |
+
active = bool(switchsel.get("active"))
|
| 1012 |
+
if not active:
|
| 1013 |
+
# enter switch-dims selection mode
|
| 1014 |
+
return path, subsel, {"active": True, "start_idx": None, "end_idx": None}
|
| 1015 |
+
else:
|
| 1016 |
+
# cancel switch-dims selection mode
|
| 1017 |
+
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1018 |
|
| 1019 |
+
|
| 1020 |
+
# 8) Figure clicks
|
| 1021 |
if trigger == "fig" and clickData and clickData.get("points"):
|
| 1022 |
p = clickData["points"][0]
|
| 1023 |
cd = p.get("customdata")
|
| 1024 |
|
| 1025 |
+
# If we're in switch-dims mode, vertex clicks define exactly 3 consecutive vertices
|
| 1026 |
+
if switchsel.get("active") and isinstance(cd, (int, float)):
|
| 1027 |
+
vid = int(cd)
|
| 1028 |
+
idx = index_in_path(path, vid)
|
| 1029 |
+
if idx is None:
|
| 1030 |
+
return path, subsel, switchsel
|
| 1031 |
+
|
| 1032 |
+
s = switchsel.get("start_idx")
|
| 1033 |
+
e = switchsel.get("end_idx")
|
| 1034 |
+
|
| 1035 |
+
# first vertex
|
| 1036 |
+
if s is None:
|
| 1037 |
+
return path, subsel, {"active": True, "start_idx": idx, "end_idx": idx}
|
| 1038 |
+
|
| 1039 |
+
if e is None:
|
| 1040 |
+
e = s
|
| 1041 |
+
|
| 1042 |
+
# must extend consecutively forward: idx == e+1
|
| 1043 |
+
if idx == e + 1 and (idx - s) <= 2:
|
| 1044 |
+
new_sel = {"active": True, "start_idx": s, "end_idx": idx}
|
| 1045 |
+
|
| 1046 |
+
# if we have 3 vertices selected: apply switch
|
| 1047 |
+
if idx == s + 2:
|
| 1048 |
+
x = path[s]
|
| 1049 |
+
y = path[s + 1]
|
| 1050 |
+
z = path[s + 2]
|
| 1051 |
+
|
| 1052 |
+
a = edge_dimension(x, y)
|
| 1053 |
+
b = edge_dimension(y, z)
|
| 1054 |
+
if a is None or b is None:
|
| 1055 |
+
# not a valid 2-edge segment, exit mode
|
| 1056 |
+
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1057 |
+
|
| 1058 |
+
# new middle vertex to traverse b then a
|
| 1059 |
+
y2 = x ^ (1 << b)
|
| 1060 |
+
|
| 1061 |
+
# optional safety: don't create duplicates elsewhere in the path
|
| 1062 |
+
if y2 in set(path) and y2 not in {x, y, z}:
|
| 1063 |
+
return path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1064 |
+
|
| 1065 |
+
new_path = path[:]
|
| 1066 |
+
new_path[s + 1] = y2
|
| 1067 |
+
return new_path, subsel, {"active": False, "start_idx": None, "end_idx": None}
|
| 1068 |
+
|
| 1069 |
+
return path, subsel, new_sel
|
| 1070 |
+
|
| 1071 |
+
# allow shrinking by clicking current end
|
| 1072 |
+
if idx == e:
|
| 1073 |
+
new_e = e - 1 if e > s else s
|
| 1074 |
+
return path, subsel, {"active": True, "start_idx": s, "end_idx": new_e}
|
| 1075 |
+
|
| 1076 |
+
# otherwise ignore
|
| 1077 |
+
return path, subsel, switchsel
|
| 1078 |
+
|
| 1079 |
# If we're in subpath selection mode, vertex clicks define (start_idx, end_idx)
|
| 1080 |
if subsel.get("active") and isinstance(cd, (int, float)):
|
| 1081 |
vid = int(cd)
|
| 1082 |
idx = index_in_path(path, vid)
|
| 1083 |
if idx is None:
|
| 1084 |
+
return path, subsel, switchsel
|
| 1085 |
|
| 1086 |
s = subsel.get("start_idx")
|
| 1087 |
e = subsel.get("end_idx")
|
| 1088 |
|
| 1089 |
# first selected vertex
|
| 1090 |
if s is None:
|
| 1091 |
+
return path, {"active": True, "start_idx": idx, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None}
|
| 1092 |
|
| 1093 |
# enforce consecutive forward selection along the path
|
| 1094 |
# user must click idx == e+1 to extend
|
| 1095 |
if e is None:
|
| 1096 |
e = s
|
| 1097 |
if idx == e + 1:
|
| 1098 |
+
return path, {"active": True, "start_idx": s, "end_idx": idx}, {"active": False, "start_idx": None, "end_idx": None}
|
| 1099 |
|
| 1100 |
# allow shrinking by clicking the current end again
|
| 1101 |
if idx == e:
|
| 1102 |
# pop last selected (shrink by 1) if possible
|
| 1103 |
new_e = e - 1 if e > s else s
|
| 1104 |
+
return path, {"active": True, "start_idx": s, "end_idx": new_e}, {"active": False, "start_idx": None, "end_idx": None}
|
| 1105 |
|
| 1106 |
# otherwise ignore
|
| 1107 |
+
return path, subsel, switchsel
|
| 1108 |
|
| 1109 |
# Normal mode: your existing click-to-build-path behavior
|
| 1110 |
if isinstance(cd, (int, float)):
|
| 1111 |
vid = int(cd)
|
| 1112 |
|
| 1113 |
if not path:
|
| 1114 |
+
return [vid], subsel, switchsel
|
| 1115 |
|
| 1116 |
if vid == path[-1]:
|
| 1117 |
+
return path[:-1], subsel, switchsel
|
| 1118 |
|
| 1119 |
if len(path) >= 2 and vid == path[-2]:
|
| 1120 |
+
return path[:-1], subsel, switchsel
|
| 1121 |
|
| 1122 |
if hamming_dist(vid, path[-1]) == 1:
|
| 1123 |
+
return path + [vid], subsel, switchsel
|
| 1124 |
|
| 1125 |
+
return [vid], subsel, switchsel
|
| 1126 |
|
| 1127 |
if isinstance(cd, (list, tuple)) and len(cd) == 2:
|
| 1128 |
u, v = int(cd[0]), int(cd[1])
|
| 1129 |
if not path:
|
| 1130 |
+
return [u, v], subsel, switchsel
|
| 1131 |
last = path[-1]
|
| 1132 |
if last == u:
|
| 1133 |
+
return path + [v], subsel, switchsel
|
| 1134 |
if last == v:
|
| 1135 |
+
return path + [u], subsel, switchsel
|
| 1136 |
+
return [u, v], subsel, switchsel
|
| 1137 |
|
| 1138 |
+
return path, subsel, switchsel
|
| 1139 |
|
| 1140 |
|
| 1141 |
@app.callback(
|
|
|
|
| 1332 |
labelStyle={"display": "inline-block", "background-color": "yellow"},
|
| 1333 |
)
|
| 1334 |
|
| 1335 |
+
@app.callback(
|
| 1336 |
+
Output("btn_switch_dims", "style"),
|
| 1337 |
+
Input("switch_dims_store", "data"),
|
| 1338 |
+
)
|
| 1339 |
+
def style_switch_dims_button(switchsel):
|
| 1340 |
+
switchsel = switchsel or {}
|
| 1341 |
+
active = bool(switchsel.get("active"))
|
| 1342 |
+
|
| 1343 |
+
base = {
|
| 1344 |
+
"color": "white",
|
| 1345 |
+
"border": "none",
|
| 1346 |
+
"padding": "6px 12px",
|
| 1347 |
+
"borderRadius": "8px",
|
| 1348 |
+
"cursor": "pointer",
|
| 1349 |
+
}
|
| 1350 |
|
| 1351 |
+
return {**base, "background": "#DC2626"} if active else {**base, "background": "#6B7280"}
|
| 1352 |
+
|
| 1353 |
if __name__ == "__main__":
|
| 1354 |
import os
|
| 1355 |
port = int(os.environ.get("PORT", "7860")) # HF uses 7860
|