Py.Cafe

antonymilne/

highlight-country-filter-vizro

Highlight Country and Filter Table with Vizro

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
######################################################################
# Steps to generate YAML configuration from Python dashboard:
# 1. Insert your dashboard configuration in block below.
# 2. Copy and paste YAML output from terminal.
# 3. Replace references to <REPLACE ME>.
# 4. Check the YAML configuration works by running it in this app:
######################################################################
import vizro.plotly.express as px
import vizro.models as vm
import vizro.actions as va
from vizro.models.types import capture
from vizro import Vizro
from vizro.tables import dash_ag_grid

selected_countries = [
    "Singapore",
    "Malaysia",
    "Thailand",
    "Indonesia",
    "Philippines",
    "Vietnam",
    "Cambodia",
    "Myanmar",
]

gapminder = px.data.gapminder().query("country.isin(@selected_countries)")


@capture("graph")
def bar_with_highlight(data_frame, highlight_country=None):  
    country_is_highlighted = data_frame["country"] == highlight_country  
    fig = px.bar(
        data_frame,
        x="lifeExp",
        y="country",
        labels={"lifeExp": "lifeExp in 2007"},
        color=country_is_highlighted,
        category_orders={"country": sorted(data_frame["country"]), "color": [False, True]},  
    )
    fig.update_layout(showlegend=False)
    return fig


page = vm.Page(
    title="Self-highlight a graph and cross-filter",
    components=[
        vm.Graph(
            id="bar_chart",   
            figure=bar_with_highlight(gapminder.query("year == 2007")),
            header="💡 Click on a bar to highlight the selected country and filter the table below",
            actions=[
                va.set_control(control="highlight_parameter", value="y"),   
                va.set_control(control="country_filter", value="y"),   
            ],
        ),
        vm.AgGrid(id="gapminder_table", figure=dash_ag_grid(data_frame=gapminder)),   
    ],
    controls=[
        vm.Parameter(
            id="highlight_parameter",  
            targets=["bar_chart.highlight_country"],  
            selector=vm.RadioItems(options=["NONE", *selected_countries]),  
            visible=False,  
        ),
        vm.Filter(id="country_filter", column="country", targets=["gapminder_table"], visible=False),  
    ],
)

dashboard = vm.Dashboard(pages=[page])
# DON'T PUT Vizro().build(dashboard).run() here or the app won't work.
# The last line above should just define dashboardvariable.
######################################################################

import pyaml
import json
import vizro.tables as vt
import vizro.tables as vf
import vizro.plotly.express as px

# All this bit written by ChatGPT but seems to work well...
def serialize(obj):
    if isinstance(obj, vm.types.CapturedCallable):
        # return a dict representation
        name = obj._function.__name__
        if not hasattr(px, name) and not hasattr(vt, name) and not hasattr(vf, name):
            # Hack that assumes captured callable is written in the app.py file if it's not one of the 
            # built in functions.
            name = f"__main__.{name}"
        arguments = dict(obj._arguments)
        if "data_frame" in arguments:
            arguments["data_frame"] = "<REPLACE ME>"
        return {"_target_": name, **arguments}
    raise TypeError(f"Type {type(obj)} not serializable")

def insert_type(base, override):
    if isinstance(base, dict) and isinstance(override, dict):
        # First, recurse into all keys in base
        result = {}
        for k, v in base.items():
            result[k] = insert_type(v, override.get(k, {}))
        # Then, insert "type" from override if present at this level
        if "type" in override:
            result["type"] = override["type"]
        return result
    elif isinstance(base, list) and isinstance(override, list):
        # recurse element-wise for lists (assumes matching lengths)
        return [insert_type(b, o) for b, o in zip(base, override)]
    else:
        return base  # leave scalars / non-dict / non-list as-is

# We want a the dumped version to exclude unset fields but include `type`. There doesn't seem to be an easy way to do this 
# with pydantic (maybe possible using Field-level include though, not sure?). So we just manually generate both and then 
# take the type key out of the second one and insert into the first.
# UPDATE: in recent pydantic there's exclude_if as a field which would probably do this for us more cleanly.
dict1 = dashboard.model_dump(exclude_unset=True)
dict2 = dashboard.model_dump()
result = insert_type(dict1, dict2)

# If we built this into Vizro then this serialization should be built into CapturedCallable itself rather than needing to 
# use it in json.dumps.
json_str = json.dumps(result, default=serialize)

# Overall we do dashboard model -> dict -> json string -> dict -> yaml string. 
# If we built serialization into model_dump itself then this could just be done as dashboard model -> dict -> yaml string.
# There are other ways to directly do dashboard model -> yaml string (e.g. pydantic-yaml, yaml.add_representer) but probably not worth 
# pursuing - we should instead keep JSON/dict as our "source of truth" rather than using specific techniques.
# Format using pretty yaml seems more similar to our preferred using prettier.
print(pyaml.dump(json.loads(json_str), vspacing=dict(split_lines=1000)))


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