Okay, here is the code, hold on to your hats ! (it’s a bit complicated
)
Template Part
This how the “card” is created (it removed some useless lines):
def create_card(n, question_ids):
"""
n = n_clicks from the callback or 1 if it's the first card created by default
question_ids = a list of strings referencing to IDs
"""
# The content ID
content_id = {"type": "rc-label-card", "index": "rc-{}".format(n)}
# The label ID
label_id = {"type": "rc-value-label", "index": "rc-{}".format(n)}
label_input = dbc.Input(id=label_id)
# An hidden index needed for managing stuffs
index_id = {"type": "rc-index-label", "index": "rc-{}".format(n)}
index_input = dbc.Input(id=index_id, type="hidden")
# The delete btn ID
btn_id = {"type": "rc-delete-label", "index": "rc-{}".format(n)}
btn = dbc.Button(html.I(className="bi bi-x-lg"), id=btn_id, color="danger", size="sm", className="float-end")
# Add condition btn
add_condition_id = {"type": "rc-add-condition", "index": "rc-{}".format(n)}
add_condition_btn = dbc.Button(
[html.I(className="bi bi-plus-lg text-primary")],
id=add_condition_id,
)
# Then a lot of template stuff with dbc...
# And I build the first blank condition when the card is created
# With the following function call
condition = create_condition(n, 1, question_ids)
card_body = [] # I dropped those lines, not usefull
return dbc.Card(card_body, className="mt-2")
And how create_condition works:
def create_condition(n_card, n_condition, question_ids):
"""
n_card: n_clicks from create_card function, used for ID
n_condition: n_clicks from callback when the + btn is clicked to add a condition
question_ids = a list of strings referencing to IDs
"""
# For each input, the ID format is rc-n_card-n_condition
questions_id = {"type": "rc-question", "index": "rc-{}-{}".format(n_card, n_condition)}
dd_question = dcc.Dropdown(options=[{"label": q, "value": q} for q in question_ids], id=questions_id, multi=False, clearable=True)
operators = ["==", "!=", ">=", ">", "<=", "<", "in", "not in", "between"]
operator_id = {"type": "rc-operator", "index": "rc-{}-{}".format(n_card, n_condition)}
# The code for create_operators_input is shown below
dd_operator = create_operators_input(n_card, n_condition, "single")
values_id = {"type": "rc-values", "index": "rc-{}-{}".format(n_card, n_condition)}
# The code for create_values_input is shown below
dd_values = create_values_input(n_card, n_condition)
expand_id = {"type": "rc-expand", "index": "rc-{}-{}".format(n_card, n_condition)}
check_expand = dbc.Checklist(
options=[
{"label": "", "value": 1},
],
value=[1],
id=expand_id,
)
index_id = {"type": "rc-condition-index", "index": "rc-{}-{}".format(n_card, n_condition)}
input_index = dbc.Input(type="hidden", id=index_id)
update_input_id = {"type": "rc-update-input", "index": "rc-{}-{}".format(n_card, n_condition)}
update_operators_id = {"type": "rc-update-operators", "index": "rc-{}-{}".format(n_card, n_condition)}
return dbc.Row([
# Template
])
And the two function to create dynamic inputs based on types I have in my dataframe:
def create_values_input(card, cond, input_type="dropdown", options=(), min_value=-1000, max_value=1000, step=1, operator=None):
values_id = {"type": "rc-values", "index": "rc-{}-{}".format(card, cond)}
dd_values = dcc.Dropdown(options=options, id=values_id, multi=True, clearable=False)
if input_type == "number":
dd_values = dbc.Input(type=input_type, id=values_id, min=min_value, max=max_value, step=step)
if input_type == "number" and operator in ["between", "not in", "in"]:
dd_values = dbc.Input(type="text", id=values_id)
return dd_values
def create_operators_input(card, cond, condition_type, actual_value="=="):
operators = ["==", "!=", ">=", ">", "<=", "<", "in", "not in", "between"]
operator_id = {"type": "rc-operator", "index": "rc-{}-{}".format(card, cond)}
if condition_type in ["single", "multi", "grid"]:
if actual_value in [">=", ">", "<=", "<"]:
actual_value = "in"
operators = ["in", "not in"]
return dcc.Dropdown(options=[{"label": o, "value": o} for o in operators], value=actual_value, id=operator_id, multi=False, clearable=False)
Callback Part
This is the callback that creates/deletes a card (label), works great:
@app.callback(
Output("rc-cards-content", "children"),
Input("rc-add-label", "n_clicks"),
Input({"type": "rc-delete-label", "index": ALL}, "n_clicks"),
State("rc-study", "value"),
State("rc-cards-content", "children"),
prevent_initial_call=True
)
def add_label(n, d, study, children):
action = dash.callback_context.triggered[0]["prop_id"].split(".")[0]
if "rc-delete-label" in action:
delete_card = json.loads(action)["index"]
children = [
card for card in children
if "'index': '" + str(delete_card) + "'" not in str(card)
]
return children
# Functions and parameters
# Not usefull to understand
s, w = study.split("-")
meta = fetch_metadata(s, w)
not_in = ["x", "y"]
meta = meta.query("state == 1 & ctype not in @not_in")
question_ids = meta["question_id"].unique()
# Here is called create_card
children.append(create_card(n, question_ids))
return children
This one create a condition inside a card, works fine (I don’t know how to delete it, too many dynamic callback for my brain):
@app.callback(
Output({"type": "rc-label-card", "index": MATCH}, "children"),
Input({"type": "rc-add-condition", "index": MATCH}, "n_clicks"),
State("rc-study", "value"),
State({"type": "rc-label-card", "index": MATCH}, "children"),
prevent_initial_call=True
)
def add_condition(n, study, children):
action = dash.callback_context.triggered[0]["prop_id"].split(".")[0]
action_index = json.loads(action)["index"]
if "add" in action_type:
s, w = study.split("-")
meta = fetch_metadata(s, w)
not_in = ["x", "y"]
meta = meta.query("state == 1 & ctype not in @not_in")
question_ids = meta["question_id"].unique()
c = action_index.split("-")[1]
children.append(create_condition(c, n, question_ids))
return children
And I guess, this is where the issue happens:
@app.callback(
Output({"type": "rc-update-input", "index": MATCH}, "children"),
Output({"type": "rc-update-operators", "index": MATCH}, "children"),
[
Input({"type": "rc-question", "index": MATCH}, "value"),
Input({"type": "rc-operator", "index": MATCH}, "value"),
],
State("rc-study", "value"),
prevent_initial_call=True
)
def update_value_input(question_id, operator, study):
s, w = study.split("-")
action = dash.callback_context.triggered[0]["prop_id"].split(".")[0]
_, card, cond = json.loads(action)["index"].split("-")
meta = fetch_metadata(s, w)
not_in = ["x", "y"]
meta = meta.query("question_id == @question_id & ctype not in @not_in")
ctypes = meta["ctype"].unique()
if "single" in ctype or "multi" in ctypes:
opts = [
{"label": l, "value": v}
for v, l in zip(meta["answer_label"], meta["answer_label"].apply(lambda x: x[: 50]+"..." if len(x) > 50 else x))
]
new_input = create_values_input(card, cond, options=opts)
new_operators = create_operators_input(card, cond, confirmit_type, operator)
return new_input, new_operators
if "grid" in confirmit_type:
meta_cleaned = meta.groupby(["answer_code", "answer_label"]).size().reset_index()
opts = [
{"label": l, "value": v}
for v, l in zip(meta_cleaned["answer_label"], meta_cleaned["answer_label"].apply(lambda x: x[: 50]+"..." if len(x) > 50 else x))
]
new_input = create_values_input(card, cond, options=opts)
new_operators = create_operators_input(card, cond, confirmit_type, operator)
return new_input, new_operators
if "numeric" in confirmit_type or "numericlist" in confirmit_type:
new_input = create_values_input(card, cond, input_type="number", operator=operator)
new_operators = create_operators_input(card, cond, confirmit_type, operator)
return new_input, new_operators
return dash.no_update, dash.no_update
That last callback updates the ‘condition’ and ‘values’ inputs. It works almost great.
If I change the value in the first Question ID dropdown (yes, user may be wrong…
), it’s replacing ‘condition’ and ‘values’ depending on the Question ID attributes.
But, it seems to change the order of my inputs, like it’s the last updated that comes first. That’s not the behaviour I want since it breaks all my conditions (cf. first post).
I hope I am clear enough.
Thanks a lot for you help.