Skip to content

Commit

Permalink
Merge branch 'main' into dev/arrange_containers
Browse files Browse the repository at this point in the history
  • Loading branch information
huong-li-nguyen committed Dec 21, 2023
2 parents 5adc769 + ec4f6fb commit 1511e10
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 118 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Added
- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Changed
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Fixed
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 23 additions & 7 deletions vizro-core/docs/pages/user_guides/custom_charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ def minimal_example(data_frame:pd.DataFrame=None):
return go.Figure()
```

Building on the above, there are several routes one can take. The following examples are guides on the most common custom requests, but also serve as an illustration of more general principles:
Building on the above, there are several routes one can take. The following examples are guides on the most common custom requests, but also serve as an illustration of more general principles.

!!! tip

Custom charts can be targeted by [Filters](filters.md) or [Parameters](parameters.md) without any additional configuration. We will showcase both possibilities in the following examples. In particular the `Parameters` in combination with custom charts can be highly versatile in achieving custom functionality.


## Enhanced `plotly.express` chart with reference line

!!! example "Custom waterfall chart"
The below examples shows a case where we enhance an existing `plotly.express` chart. We add a new argument (`hline`), that is used to draw a grey reference line at the height determined by the value of `hline`. The important thing to note is that we then
add a `Parameter` that allows the dashboard user to interact with the argument, and hence move the line in this case. See the `Result` tab for an animation.

!!! example "Custom `plotly.express` scatter chart with a `Parameter`"
=== "app.py"
```py
import vizro.models as vm
Expand All @@ -43,7 +51,7 @@ Building on the above, there are several routes one can take. The following exam


@capture("graph")
def scatter_with_line(data_frame, x, y, color=None, size=None, hline=None):
def scatter_with_line(data_frame, x, y, color=None, size=None, hline=None): # (1)!
fig = px.scatter(data_frame=data_frame, x=x, y=y, color=color, size=size)
fig.add_hline(y=hline, line_color="gray")
return fig
Expand All @@ -65,28 +73,34 @@ Building on the above, there are several routes one can take. The following exam
),
],
controls=[
vm.Filter(column="petal_width"),
vm.Parameter( # (2)!
targets=["enhanced_scatter.hline"],
selector=vm.Slider(min=2, max=5, step=1, value=3, title="Horizontal line"),
),
],
)
dashboard = vm.Dashboard(pages=[page_0])

Vizro().build(dashboard).run()
```

1. Note that arguments of the custom chart can be parametrized. Here we choose to parametrize the `hline` argument (see below).
2. Here we parametrize the `hline` argument, but any other argument can be parametrized as well. Since there is complete flexibility regarding what can be derived from such arguments, the dashboard user has a wide range of customization options.
=== "app.yaml"
```yaml
# Custom charts are currently only possible via python configuration
```
=== "Result"
[![Graph2]][Graph2]

[Graph2]: ../../assets/user_guides/custom_charts/custom_chart_enhanced_scatter.png
[Graph2]: ../../assets/user_guides/custom_charts/custom_chart_showcase_parameter.gif


## New Waterfall chart based on `go.Figure()`

The below examples shows a more involved use-case. We create and style a waterfall chart, and add it alongside a filter to the dashboard. The example is based on [this](https://plotly.com/python/waterfall-charts/) tutorial.

!!! example "Custom waterfall chart"
!!! example "Custom `go.Figure()` waterfall chart with a `Parameter`"
=== "app.py"
```py
import pandas as pd
Expand Down Expand Up @@ -136,13 +150,15 @@ The below examples shows a more involved use-case. We create and style a waterfa
),
],
controls=[
vm.Filter(column="x", selector=vm.Dropdown(title="Financial categories", multi=True)),
vm.Filter(column="x", selector=vm.Dropdown(title="Financial categories", multi=True)),# (1)!
],
)
dashboard = vm.Dashboard(pages=[page_0])

Vizro().build(dashboard).run()
```

1. Note how we are able to apply a filter to a custom chart
=== "app.yaml"
```yaml
# Custom charts are currently only possible via python configuration
Expand Down
138 changes: 27 additions & 111 deletions vizro-core/docs/pages/user_guides/custom_components.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# How to create custom components

If you can't find a component that you would like to have in the code basis, you can easily create your own custom component.
This guide shows you how to create custom components or enhance existing ones.
Vizro's public API is deliberately kept small in order to facilitate quick and easy configuration of a dashboard. However,
at the same time, Vizro is easily extensible, so that you can tweak any component to your liking or even create entirely new ones.

If you can't find a component that you would like to have in the code basis, or if you would like to alter/enhance an existing component, then you are in the right place.
This guide shows you how to create custom components that are completely new, or enhancements of existing ones.

In general, you can create a custom component based on any dash-compatible component (e.g. [dash-core-components](https://dash.plotly.com/dash-core-components),
[dash-bootstrap-components](https://dash-bootstrap-components.opensource.faculty.ai/), [dash-html-components](https://github.com/plotly/dash/tree/dev/components/dash-html-components), etc.).


Vizro's public API is deliberately kept small in order to facilitate quick and easy configuration of a dashboard. However,
at the same time, Vizro is easily extensible, so that you can tweak any component to your liking or even create entirely new ones.
All our components are based on `Dash`, and they are shipped with a set of sensible defaults that can be modified. If you would like to overwrite one of those defaults,
or if you would like to use additional `args` or `kwargs` of those components, then this is the correct way to include those. You can very easily use any existing attribute of any underlying Dash component with this method.

!!!note

Expand All @@ -35,12 +37,11 @@ at the same time, Vizro is easily extensible, so that you can tweak any componen
- change any fields of any models (e.g. changing the title field from `Optional` to have a default)


You can extend an existing component by sub-classing the component you want to alter.
The below example is a bit lengthy, but the annotations should guide you through the most important lines of that code. Remember that when sub-classing a component
You can extend an existing component by sub-classing the component you want to alter. Remember that when sub-classing a component
you have access to all fields of all parent models, but you can choose to overwrite any field or method, or define new ones.

The aim for this example is to enhance the [`RangeSlider`][vizro.models.RangeSlider] model so that
one slider handle cannot cross the other, and to have a permanent tooltip showing the current value. You will note that it is often easier to copy parts of the source-code when needing to change complex methods
one slider handle cannot cross the other, and to have a permanent tooltip showing the current value. You will note that it is often easier to call `super()` when overriding a complex method
such as the `build` method in the below example instead of attempting to write it from scratch.

In this case, the general three steps translate into:
Expand Down Expand Up @@ -73,8 +74,7 @@ vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider)
??? example "Example based on existing component"

=== "app.py"
``` py linenums="1" hl_lines="66 67"
from dash import Input, Output, State, callback, callback_context, dcc, html
``` py linenums="1" hl_lines="18 19"
from typing_extensions import Literal

import vizro.models as vm
Expand All @@ -88,122 +88,37 @@ vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider)
class TooltipNonCrossRangeSlider(vm.RangeSlider):
"""Custom numeric multi-selector `TooltipNonCrossRangeSlider`."""

type: Literal["other_range_slider"] = "other_range_slider" # (1)!
type: Literal["other_range_slider"] = "other_range_slider" # (1)!

def build(self): # (2)!
value = self.value or [self.min, self.max] # type: ignore[list-item]

output = [
Output(f"{self.id}_start_value", "value"),
Output(f"{self.id}_end_value", "value"),
Output(self.id, "value"),
Output(f"temp-store-range_slider-{self.id}", "data"),
]
input = [
Input(f"{self.id}_start_value", "value"),
Input(f"{self.id}_end_value", "value"),
Input(self.id, "value"),
State(f"temp-store-range_slider-{self.id}", "data"),
]

@callback(output=output, inputs=input)
def update_slider_values(start, end, slider, input_store):
trigger_id = callback_context.triggered_id
if trigger_id == f"{self.id}_start_value" or trigger_id == f"{self.id}_end_value":
start_text_value, end_text_value = start, end
elif trigger_id == self.id:
start_text_value, end_text_value = slider
else:
start_text_value, end_text_value = input_store if input_store is not None else value

start_value = min(start_text_value, end_text_value)
end_value = max(start_text_value, end_text_value)
start_value = max(self.min, start_value)
end_value = min(self.max, end_value)
slider_value = [start_value, end_value]
return start_value, end_value, slider_value, (start_value, end_value)

return html.Div(
[
html.P(self.title, id="range_slider_title") if self.title else None,
html.Div(
[
dcc.RangeSlider(
id=self.id,
min=self.min,
max=self.max,
step=self.step,
marks=self.marks,
className="range_slider_control" if self.step else "range_slider_control_no_space",
value=value,
persistence=True,
persistence_type="session",
allowCross=False, # (3)!
tooltip={"placement": "bottom", "always_visible": True}, # (4)!
),
html.Div(
[
dcc.Input(
id=f"{self.id}_start_value",
type="number",
placeholder="start",
min=self.min,
max=self.max,
className="slider_input_field_left"
if self.step
else "slider_input_field_no_space_left",
value=value[0],
size="24px",
persistence=True,
persistence_type="session",
),
dcc.Input(
id=f"{self.id}_end_value",
type="number",
placeholder="end",
min=self.min,
max=self.max,
className="slider_input_field_right"
if self.step
else "slider_input_field_no_space_right",
value=value[1],
persistence=True,
persistence_type="session",
),
dcc.Store(id=f"temp-store-range_slider-{self.id}", storage_type="session"),
],
className="slider_input_container",
),
],
className="range_slider_inner_container",
),
],
className="selector_container",
)
range_slider_build_obj = super().build() # (3)!
range_slider_build_obj[self.id].allowCross = False # (4)!
range_slider_build_obj[self.id].tooltip = {"always_visible": True, "placement": "bottom"} # (5)!
return range_slider_build_obj


# 2. Add new components to expected type - here the selector of the parent components
vm.Filter.add_type("selector", TooltipNonCrossRangeSlider) # (5)!
vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider) # (6)!
vm.Filter.add_type("selector", TooltipNonCrossRangeSlider) # (6)!
vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider) # (7)!

page = vm.Page(
title="Custom Component",
path="custom-component",
components=[
vm.Graph(
id="for_custom_chart",
figure=px.scatter(iris, title="Foo", x="sepal_length", y="petal_width", color="sepal_width"),
figure=px.scatter(iris, title="Iris Dataset", x="sepal_length", y="petal_width", color="sepal_width"),
),
],
controls=[
vm.Filter(
column="sepal_length",
targets=["for_custom_chart"],
selector=TooltipNonCrossRangeSlider(), # (7)!
selector=TooltipNonCrossRangeSlider(),
),
vm.Parameter(
targets=["for_custom_chart.range_x"],
selector=TooltipNonCrossRangeSlider(title="Select x-axis range", min=0, max=10),
selector=TooltipNonCrossRangeSlider(title="Select x-axis range", min=0, max=10), # (8)!
),
],
)
Expand All @@ -214,12 +129,13 @@ vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider)
```

1. Here we provide a new type for the new component, so it can be distinguished in the discriminated union.
2. Here we chose to mostly copy the source code of the build method and alter it directly. Alternatively we could overload the `build` method and alter the output of `super().build()`, but then we recommend pinning the version of `Vizro` in case of future breaking changes.
3. This is the change that makes the `RangeSlider` not cross itself when moving the handle.
4. This is the change that displays the tooltip below the handle.
5. **Don't forget!** If part of a discriminated union, you must add the new component to the parent model where it will be inserted. In this case the new `TooltipNonCrossRangeSlider` will be inserted into the `selector` argument of the `Filter` model, and thus must be added as an allowed type.
6. **Don't forget!** If part of a discriminated union, you must add the new component to the parent model where it will be inserted. In this case the new `TooltipNonCrossRangeSlider` will be inserted into the `selector` argument of the `Parameter` model, and thus must be added as an allowed type.
7. The new component can now be inserted into a regular dashboard.
2. Here we override the `build` method by altering the output of `super().build()`. Alternatively one could copy the source code of the build method and alter it directly.
3. `range_slider_build_obj[self.id]` then fetches the underlying [`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider) object.
4. This change prevents the `RangeSlider` from crossing itself when moving the handle.
5. This change displays the tooltip below the handle.
6. **Remember!** If part of a discriminated union, you must add the new component to the parent model where it will be inserted. In this case the new `TooltipNonCrossRangeSlider` will be inserted into the `selector` argument of the `Filter` model, and thus must be added as an allowed type.
7. **Remember!** If part of a discriminated union, you must add the new component to the parent model where it will be inserted. In this case the new `TooltipNonCrossRangeSlider` will be inserted into the `selector` argument of the `Parameter` model, and thus must be added as an allowed type.
8. The new component can now be inserted into a regular dashboard.

=== "yaml"
```yaml
Expand Down

0 comments on commit 1511e10

Please sign in to comment.