diff --git a/.github/images/tech_logos.png b/.github/images/tech_logos.png index aebb0cc2d..38da531d7 100644 Binary files a/.github/images/tech_logos.png and b/.github/images/tech_logos.png differ diff --git a/vizro-core/changelog.d/20231016_131411_nadija_ratkusic_graca_components_range_slider_mark_step_bugfix.md b/vizro-core/changelog.d/20231016_131411_nadija_ratkusic_graca_components_range_slider_mark_step_bugfix.md new file mode 100644 index 000000000..633842c40 --- /dev/null +++ b/vizro-core/changelog.d/20231016_131411_nadija_ratkusic_graca_components_range_slider_mark_step_bugfix.md @@ -0,0 +1,41 @@ + + + + + + + +### Fixed + +- Enable turning off `marks` when `step` is defined in `Slider` and `RangeSlider` ([#115](https://github.com/mckinsey/vizro/pull/115)) + + diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index f866fab4c..989901141 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -459,7 +459,7 @@ def create_country_analysis(): ], controls=[ vm.Filter(column="country", selector=vm.Dropdown(value="India", multi=False, title="Select country")), - vm.Filter(column="year", selector=vm.RangeSlider(title="Select timeframe")), + vm.Filter(column="year", selector=vm.RangeSlider(title="Select timeframe", step=1, marks=None)), ], ) return page_country diff --git a/vizro-core/schemas/0.1.5.dev0.json b/vizro-core/schemas/0.1.5.dev0.json index 1c21146e5..a1cf7a7fc 100644 --- a/vizro-core/schemas/0.1.5.dev0.json +++ b/vizro-core/schemas/0.1.5.dev0.json @@ -501,7 +501,7 @@ }, "RangeSlider": { "title": "RangeSlider", - "description": "Numeric multi-selector `RangeSlider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `None`.\n value (Optional[List[float]]): Default start and end value for slider. Must be 2 items. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "description": "Numeric multi-selector `RangeSlider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`.\n value (Optional[List[float]]): Default start and end value for slider. Must be 2 items. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", "type": "object", "properties": { "id": { @@ -533,6 +533,7 @@ "marks": { "title": "Marks", "description": "Marks to be displayed on slider.", + "default": {}, "type": "object", "additionalProperties": { "type": "string" @@ -566,7 +567,7 @@ }, "Slider": { "title": "Slider", - "description": "Numeric single-selector `Slider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.Slider`](https://dash.plotly.com/dash-core-components/slider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `None`.\n value (Optional[float]): Default value for slider. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "description": "Numeric single-selector `Slider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.Slider`](https://dash.plotly.com/dash-core-components/slider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`.\n value (Optional[float]): Default value for slider. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", "type": "object", "properties": { "id": { @@ -598,6 +599,7 @@ "marks": { "title": "Marks", "description": "Marks to be displayed on slider.", + "default": {}, "type": "object", "additionalProperties": { "type": "string" diff --git a/vizro-core/src/vizro/models/_components/form/_form_utils.py b/vizro-core/src/vizro/models/_components/form/_form_utils.py index 2be53ef3c..1780ec583 100644 --- a/vizro-core/src/vizro/models/_components/form/_form_utils.py +++ b/vizro-core/src/vizro/models/_components/form/_form_utils.py @@ -94,5 +94,7 @@ def validate_step(cls, step, values): return step -def set_default_marks(cls, v, values): - return v if values.get("step") is None else {} +def set_default_marks(cls, marks, values): + if not marks and values.get("step") is None: + marks = None + return marks diff --git a/vizro-core/src/vizro/models/_components/form/range_slider.py b/vizro-core/src/vizro/models/_components/form/range_slider.py index 425b183f6..fc38daf75 100644 --- a/vizro-core/src/vizro/models/_components/form/range_slider.py +++ b/vizro-core/src/vizro/models/_components/form/range_slider.py @@ -26,7 +26,7 @@ class RangeSlider(VizroBaseModel): min (Optional[float]): Start value for slider. Defaults to `None`. max (Optional[float]): End value for slider. Defaults to `None`. step (Optional[float]): Step-size for marks on slider. Defaults to `None`. - marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `None`. + marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`. value (Optional[List[float]]): Default start and end value for slider. Must be 2 items. Defaults to `None`. title (Optional[str]): Title to be displayed. Defaults to `None`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. @@ -36,7 +36,7 @@ class RangeSlider(VizroBaseModel): min: Optional[float] = Field(None, description="Start value for slider.") max: Optional[float] = Field(None, description="End value for slider.") step: Optional[float] = Field(None, description="Step-size for marks on slider.") - marks: Optional[Dict[float, str]] = Field(None, description="Marks to be displayed on slider.") + marks: Optional[Dict[float, str]] = Field({}, description="Marks to be displayed on slider.") value: Optional[List[float]] = Field( None, description="Default start and end value for slider", min_items=2, max_items=2 ) @@ -105,6 +105,7 @@ def build(self): placeholder="start", min=self.min, max=self.max, + step=self.step, value=value[0], size="24px", persistence=True, @@ -118,6 +119,7 @@ def build(self): placeholder="end", min=self.min, max=self.max, + step=self.step, value=value[1], persistence=True, className="slider_input_field_right" diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py index b0349f671..291ca5030 100644 --- a/vizro-core/src/vizro/models/_components/form/slider.py +++ b/vizro-core/src/vizro/models/_components/form/slider.py @@ -26,7 +26,7 @@ class Slider(VizroBaseModel): min (Optional[float]): Start value for slider. Defaults to `None`. max (Optional[float]): End value for slider. Defaults to `None`. step (Optional[float]): Step-size for marks on slider. Defaults to `None`. - marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `None`. + marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`. value (Optional[float]): Default value for slider. Defaults to `None`. title (Optional[str]): Title to be displayed. Defaults to `None`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. @@ -36,7 +36,7 @@ class Slider(VizroBaseModel): min: Optional[float] = Field(None, description="Start value for slider.") max: Optional[float] = Field(None, description="End value for slider.") step: Optional[float] = Field(None, description="Step-size for marks on slider.") - marks: Optional[Dict[float, str]] = Field(None, description="Marks to be displayed on slider.") + marks: Optional[Dict[float, str]] = Field({}, description="Marks to be displayed on slider.") value: Optional[float] = Field(None, description="Default value for slider.") title: Optional[str] = Field(None, description="Title to be displayed.") actions: List[Action] = [] @@ -98,6 +98,7 @@ def build(self): placeholder="end", min=self.min, max=self.max, + step=self.step, value=self.value or self.min, persistence=True, className="slider_input_field_right" if self.step else "slider_input_field_no_space_right", diff --git a/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py b/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py index ca1b65ca1..a5a74407a 100644 --- a/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py +++ b/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py @@ -42,6 +42,7 @@ def expected_range_slider_default(): placeholder="start", className="slider_input_field_no_space_left", size="24px", + step=None, persistence=True, min=None, max=None, @@ -53,6 +54,7 @@ def expected_range_slider_default(): placeholder="end", className="slider_input_field_no_space_right", persistence=True, + step=None, min=None, max=None, value=None, @@ -89,8 +91,8 @@ def expected_range_slider_with_optional(): id="range_slider_with_all", min=0, max=10, - step=1, - marks={}, + step=2, + marks={1.0: "1", 5.0: "5", 10.0: "10"}, className="range_slider_control", value=[0, 10], persistence=True, @@ -102,6 +104,7 @@ def expected_range_slider_with_optional(): type="number", placeholder="start", min=0, + step=2, max=10, className="slider_input_field_left", value=0, @@ -114,6 +117,7 @@ def expected_range_slider_with_optional(): placeholder="end", min=0, max=10, + step=2, className="slider_input_field_right", value=10, persistence=True, @@ -149,13 +153,19 @@ def test_create_range_slider_mandatory_only(self): def test_create_range_slider_mandatory_and_optional(self): range_slider = vm.RangeSlider( - min=0, max=10, step=1, marks={}, value=[1, 9], title="Test title", id="range_slider_id" + min=0, + max=10, + step=1, + marks={1: "1", 5: "5", 10: "10"}, + value=[1, 9], + title="Test title", + id="range_slider_id", ) assert range_slider.min == 0 assert range_slider.max == 10 assert range_slider.step == 1 - assert range_slider.marks == {} + assert range_slider.marks == {1: "1", 5: "5", 10: "10"} assert range_slider.value == [1, 9] assert range_slider.title == "Test title" assert range_slider.id == "range_slider_id" @@ -226,20 +236,6 @@ def test_validate_step_invalid(self): ): vm.RangeSlider(min=0, max=10, step=11) - @pytest.mark.parametrize( - "marks, step, expected", - [ - ({2: "2", 4: "4", 6: "6"}, 1, {}), - ({2: "2", 4: "4", 6: "6"}, None, {2: "2", 4: "4", 6: "6"}), - ({}, 1, {}), - ], - ) - def test_step_precedence_over_marks(self, marks, step, expected): - slider = vm.RangeSlider(min=0, max=10, marks=marks, step=step) - - assert slider.marks == expected - assert slider.step == step - @pytest.mark.parametrize( "marks, expected", [ @@ -266,6 +262,19 @@ def test_set_default_marks(self, step, expected): slider = vm.RangeSlider(min=0, max=10, step=step) assert slider.marks == expected + @pytest.mark.parametrize( + "step, marks, expected", + [ + (1, None, None), + (None, {1: "1", 2: "2"}, {1: "1", 2: "2"}), + (1, {1: "1", 2: "2"}, {1: "1", 2: "2"}), + (None, {}, None), + ], + ) + def test_set_step_and_marks(self, step, marks, expected): + slider = vm.RangeSlider(min=0, max=10, step=step, marks=marks) + assert slider.marks == expected + @pytest.mark.parametrize( "title", [ @@ -301,7 +310,15 @@ def test_range_slider_build_default(self, expected_range_slider_default): assert result == expected def test_range_slider_build_with_optional(self, expected_range_slider_with_optional): - range_slider = vm.RangeSlider(min=0, max=10, step=1, value=[0, 10], id="range_slider_with_all", title="Title") + range_slider = vm.RangeSlider( + min=0, + max=10, + step=2, + value=[0, 10], + id="range_slider_with_all", + title="Title", + marks={1: "1", 5: "5", 10: "10"}, + ) component = range_slider.build() result = json.loads(json.dumps(component, cls=plotly.utils.PlotlyJSONEncoder)) diff --git a/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py b/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py index 5e9c4ed23..a8cd4a6ab 100755 --- a/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py +++ b/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py @@ -40,6 +40,7 @@ def expected_slider(): type="number", placeholder="end", min=0, + step=1, max=10, value=5, persistence=True, @@ -63,9 +64,9 @@ def test_create_slider_mandatory(self): assert hasattr(slider, "id") assert slider.type == "slider" + assert slider.step is None assert slider.min is None assert slider.max is None - assert slider.step is None assert slider.marks is None assert slider.value is None assert slider.title is None @@ -139,24 +140,10 @@ def test_validate_step_invalid(self): vm.Slider(min=0, max=10, step=11) def test_valid_marks_with_step(self): - slider = vm.Slider(min=0, max=10, step=1) + slider = vm.Slider(min=0, max=10, step=2) assert slider.marks == {} - @pytest.mark.parametrize( - "marks, step, expected", - [ - ({2: "2", 4: "4", 6: "6"}, 1, {}), - ({2: "2", 4: "4", 6: "6"}, None, {2: "2", 4: "4", 6: "6"}), - ({}, 1, {}), - ], - ) - def test_step_precedence_over_marks(self, marks, step, expected): - slider = vm.Slider(min=0, max=10, marks=marks, step=step) - - assert slider.marks == expected - assert slider.step == step - @pytest.mark.parametrize( "marks, expected", [ @@ -183,6 +170,19 @@ def test_set_default_marks(self, step, expected): slider = vm.Slider(min=0, max=10, step=step) assert slider.marks == expected + @pytest.mark.parametrize( + "step, marks, expected", + [ + (1, None, None), + (None, {1: "1", 2: "2"}, {1: "1", 2: "2"}), + (2, {1: "1", 2: "2"}, {1: "1", 2: "2"}), + (None, {}, None), + ], + ) + def test_set_step_and_marks(self, step, marks, expected): + slider = vm.Slider(min=0, max=10, step=step, marks=marks) + assert slider.marks == expected + @pytest.mark.parametrize( "title", [