Created
August 27, 2024 15:08
-
-
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"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screen.Recording.2024-08-27.at.17.09.22.mov