Skip to content

Instantly share code, notes, and snippets.

@celia-lm
Created August 27, 2024 15:08
Show Gist options
  • Save celia-lm/f16a5ed0cf4317860b2fd65d214bae6b to your computer and use it in GitHub Desktop.
Save celia-lm/f16a5ed0cf4317860b2fd65d214bae6b to your computer and use it in GitHub Desktop.
Dash AIO component to have a Number Range selector where users can specify the min and max values in two linked dcc.Input of type="number"
from dash import Dash, Output, Input, State, html, dcc, callback, MATCH
import uuid
# All-in-One Components should be suffixed with 'AIO'
class NumberRangeAIO(html.Div): # html.Div will be the "parent" component
# A set of functions that create pattern-matching callbacks of the subcomponents
class ids:
minValueInput = lambda aio_id: {
'component': 'NumberRangeAIO',
'subcomponent': 'minValue',
'aio_id': aio_id
}
maxValueInput = lambda aio_id: {
'component': 'NumberRangeAIO',
'subcomponent': 'maxValue',
'aio_id': aio_id
}
valuesInList = lambda aio_id: {
'component': 'NumberRangeAIO',
'subcomponent': 'valuesInList',
'aio_id': aio_id
}
# Make the ids class a public class
ids = ids
# Define the arguments of the All-in-One component
def __init__(
self,
limit_min=None,
limit_max=None,
minValueInput_props=None,
maxValueInput_props=None,
inputs_style=None,
aio_id=None
):
"""NumberRangeAIO is an All-in-One component that is composed
of a parent `html.Div` two dcc.Input components of number type as children.
The dcc.Input are linked; the first one represents the lower bound of a range and the second one represents the upper bound.
The max and min values are linked with a callback: for exmaple, if the value selected for the upper bound is smaller than the value
for the lower bound, the callback will overwrite it to be the minimum value allowed.
There's also an invisible dcc.RangeSlider element that stores the selected values as a list, and it is what should be targeted as Input in a callback.
- `limit_min` - Lower limit for the range. This will be the minimum allowed value for the lower bound dcc.Input. The minimun allowed value for the upper bound dcc.Input will be the current value of the other dcc.Input.
- `limit_max` - Upper limit for the range. This will be the maximum allowed value for the upper bound dcc.Inputs. The maximun allowed value for the lower bound dcc.Input will be the current value of the other dcc.Input.
- `maxValueInput_props` - A dictionary of properties passed into the lower bound dcc.Input. For example the starting value can be set with {"value":5}
- `minValueInput_props` - A dictionary of properties passed into the upper bound dcc.Input.
- `aio_id` - The All-in-One component ID used to generate the markdown and dropdown components's dictionary IDs.
The All-in-One component dictionary IDs are available as
- NumberRangeAIO.ids.minValueInput(aio_id)
- NumberRangeAIO.ids.maxValueInput(aio_id)
- NumberRangeAIO.ids.valuesInList(aio_id)
"""
# Allow developers to pass in their own `aio_id` if they're
# binding their own callback to a particular component.
if aio_id is None:
# Otherwise use a uuid that has virtually no chance of collision.
# Uuids are safe in dash deployments with processes
# because this component's callbacks
# use a stateless pattern-matching callback:
# The actual ID does not matter as long as its unique and matches
# the PMC `MATCH` pattern..
aio_id = str(uuid.uuid4())
# Merge user-supplied properties into default properties
minValueInput_props = minValueInput_props.copy() if minValueInput_props else {}
# Merge user-supplied properties into default properties
maxValueInput_props = maxValueInput_props.copy() if maxValueInput_props else {} # copy the dict so as to not mutate the user's dict
# Merge user-supplied properties into default properties
default_inputs_style ={
"width":"75px"
}
inputs_style = inputs_style.copy() if inputs_style else default_inputs_style
for d in [minValueInput_props, maxValueInput_props]:
d["type"] = "number"
d["min"] = limit_min
d["max"] = limit_max
d["style"] = inputs_style
d["debounce"] = 0.5
if "value" not in minValueInput_props.keys():
minValueInput_props["value"]=limit_min
if "value" not in maxValueInput_props.keys():
maxValueInput_props["value"]=limit_max
# Define the component's layout
super().__init__([ # Equivalent to `html.Div([...])`
dcc.Input(id=self.ids.minValueInput(aio_id), **minValueInput_props),
html.Span("–", style={"padding-left":"5px", "padding-right":"5px"}),
dcc.Input(id=self.ids.maxValueInput(aio_id), **maxValueInput_props),
html.Div(dcc.RangeSlider(id=self.ids.valuesInList(aio_id), min=limit_min, max=limit_max), style={"display":"none"})
], style={"display":"inline-flex"})
# Define this component's stateless pattern-matching callback
# that will apply to every instance of this component.
@callback(
Output(ids.valuesInList(MATCH), 'value'),
Output(ids.minValueInput(MATCH), 'max'),
Output(ids.maxValueInput(MATCH), 'min'),
Output(ids.minValueInput(MATCH), 'value'),
Output(ids.maxValueInput(MATCH), 'value'),
Input(ids.minValueInput(MATCH), 'value'),
Input(ids.maxValueInput(MATCH), 'value'),
State(ids.minValueInput(MATCH), 'min'),
State(ids.maxValueInput(MATCH), 'max'),
)
def update_markdown_style(selected_min, selected_max, abs_min, abs_max):
# when the value that the user has typed is outside of the allowed range,
# the value is "None" even if we see the new value in the Input box
new_min = abs_min if not selected_min else selected_min
new_max = abs_max if not selected_max else selected_max
selected_range = [new_min, new_max]
return selected_range, new_max, new_min, new_min, new_max
# sample app
from dash import Dash, html
app = Dash(__name__)
app.layout = html.Div([
NumberRangeAIO(limit_min=3, limit_max=20, aio_id="my_input"),
html.Div(id="out")
])
@callback(
Output("out", "children"),
Input(NumberRangeAIO.ids.valuesInList("my_input"), "value")
)
def show_values(selected_values):
return str(selected_values)
if __name__ == "__main__":
app.run(debug=True)
@celia-lm
Copy link
Author

Screen.Recording.2024-08-27.at.17.09.22.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment