Skip to content

Visualization

dissmodel.visualization.Chart

Bases: Model

Simulation model that renders a live time-series chart.

Extends :class:~dissmodel.core.Model and redraws the chart at every time step. Supports three rendering targets:

  • Streamlit — pass a plot_area (st.empty()).
  • Jupyter — detected automatically via :func:is_notebook.
  • Matplotlib window — fallback for plain Python scripts.

Parameters:

Name Type Description Default
select list of str

Subset of labels to plot. If None, all tracked variables are shown.

required
pause bool

If True, call plt.pause() after each update, by default True. Required for live updates outside notebooks.

required
plot_area any

Streamlit st.empty() placeholder, by default None.

required
show_legend bool

Whether to display the plot legend, by default True.

required
show_grid bool

Whether to display the plot grid, by default False.

required
title str

Chart title, by default "Variable History".

required

Examples:

>>> env = Environment(end_time=30)
>>> Chart(show_legend=True, show_grid=True, title="SIR Model")
>>> env.run()
Source code in dissmodel/visualization/chart.py
 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class Chart(Model):
    """
    Simulation model that renders a live time-series chart.

    Extends :class:`~dissmodel.core.Model` and redraws the chart at every
    time step. Supports three rendering targets:

    - **Streamlit** — pass a ``plot_area`` (``st.empty()``).
    - **Jupyter** — detected automatically via :func:`is_notebook`.
    - **Matplotlib window** — fallback for plain Python scripts.

    Parameters
    ----------
    select : list of str, optional
        Subset of labels to plot. If ``None``, all tracked variables are shown.
    pause : bool, optional
        If ``True``, call ``plt.pause()`` after each update, by default ``True``.
        Required for live updates outside notebooks.
    plot_area : any, optional
        Streamlit ``st.empty()`` placeholder, by default ``None``.
    show_legend : bool, optional
        Whether to display the plot legend, by default ``True``.
    show_grid : bool, optional
        Whether to display the plot grid, by default ``False``.
    title : str, optional
        Chart title, by default ``"Variable History"``.

    Examples
    --------
    >>> env = Environment(end_time=30)
    >>> Chart(show_legend=True, show_grid=True, title="SIR Model")
    >>> env.run()
    """

    fig: matplotlib.figure.Figure
    ax: matplotlib.axes.Axes
    select: Optional[list[str]]
    interval: int
    time_points: list[float]
    pause: bool
    plot_area: Any
    show_legend: bool
    show_grid: bool
    title: str

    def setup(
        self,
        select: Optional[list[str]] = None,
        pause: bool = True,
        plot_area: Any = None,
        show_legend: bool = True,
        show_grid: bool = False,
        title: str = "Variable History",
    ) -> None:
        """
        Configure the chart.

        Called automatically by salabim during component initialisation.

        Parameters
        ----------
        select : list of str, optional
            Subset of labels to plot. If ``None``, all tracked variables
            are shown.
        pause : bool, optional
            If ``True``, call ``plt.pause()`` after each update,
            by default ``True``.
        plot_area : any, optional
            Streamlit ``st.empty()`` placeholder, by default ``None``.
        show_legend : bool, optional
            Whether to display the plot legend, by default ``True``.
        show_grid : bool, optional
            Whether to display the plot grid, by default ``False``.
        title : str, optional
            Chart title, by default ``"Variable History"``.
        """
        self.select = select
        self.interval = 1
        self.time_points = []
        self.pause = pause
        self.plot_area = plot_area
        self.show_legend = show_legend
        self.show_grid = show_grid
        self.title = title

        if not is_notebook():
            self.fig, self.ax = plt.subplots()
            self.ax.set_xlabel("Time")
            self.ax.set_title(self.title)

    def execute(self) -> None:
        """
        Redraw the chart for the current simulation time step.

        Raises
        ------
        RuntimeError
            If no interactive matplotlib backend is detected and the code is
            not running in a notebook or Streamlit context.
        """
        if is_notebook():
            from IPython.display import clear_output
            clear_output(wait=True)
            self.fig, self.ax = plt.subplots()
            self.ax.set_xlabel("Time")
            self.ax.set_title(self.title)

        plt.sca(self.ax)
        self.time_points.append(self.env.now())

        plot_metadata: dict[str, Any] = getattr(self.env, "_plot_metadata", {})

        self.ax.clear()
        self.ax.set_xlabel("Time")
        self.ax.set_title(self.title)

        for label, info in plot_metadata.items():
            if self.select is None or label in self.select:
                self.ax.plot(info["data"], label=label, color=info["color"])

        if self.show_grid:
            self.ax.grid(True)

        if self.show_legend:
            self.ax.legend()

        self.ax.relim()
        self.ax.autoscale_view()
        plt.tight_layout()
        plt.draw()

        if self.plot_area is not None:
            self.plot_area.pyplot(self.fig)
        elif is_notebook():
            from IPython.display import display, Image
            buf = io.BytesIO()
            self.fig.savefig(buf, format="png")
            buf.seek(0)
            display(Image(data=buf.read()))
            plt.close(self.fig)
        elif self.pause:
            if is_interactive_backend():
                plt.pause(0.1)
                if self.env.now() == self.env.end_time:
                    plt.show()
            else:
                raise RuntimeError(
                    "No interactive matplotlib backend detected. "
                    "On Linux, install tkinter:\n\n"
                    "    sudo apt install python3-tk\n"
                )

execute()

Redraw the chart for the current simulation time step.

Raises:

Type Description
RuntimeError

If no interactive matplotlib backend is detected and the code is not running in a notebook or Streamlit context.

Source code in dissmodel/visualization/chart.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def execute(self) -> None:
    """
    Redraw the chart for the current simulation time step.

    Raises
    ------
    RuntimeError
        If no interactive matplotlib backend is detected and the code is
        not running in a notebook or Streamlit context.
    """
    if is_notebook():
        from IPython.display import clear_output
        clear_output(wait=True)
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlabel("Time")
        self.ax.set_title(self.title)

    plt.sca(self.ax)
    self.time_points.append(self.env.now())

    plot_metadata: dict[str, Any] = getattr(self.env, "_plot_metadata", {})

    self.ax.clear()
    self.ax.set_xlabel("Time")
    self.ax.set_title(self.title)

    for label, info in plot_metadata.items():
        if self.select is None or label in self.select:
            self.ax.plot(info["data"], label=label, color=info["color"])

    if self.show_grid:
        self.ax.grid(True)

    if self.show_legend:
        self.ax.legend()

    self.ax.relim()
    self.ax.autoscale_view()
    plt.tight_layout()
    plt.draw()

    if self.plot_area is not None:
        self.plot_area.pyplot(self.fig)
    elif is_notebook():
        from IPython.display import display, Image
        buf = io.BytesIO()
        self.fig.savefig(buf, format="png")
        buf.seek(0)
        display(Image(data=buf.read()))
        plt.close(self.fig)
    elif self.pause:
        if is_interactive_backend():
            plt.pause(0.1)
            if self.env.now() == self.env.end_time:
                plt.show()
        else:
            raise RuntimeError(
                "No interactive matplotlib backend detected. "
                "On Linux, install tkinter:\n\n"
                "    sudo apt install python3-tk\n"
            )

setup(select=None, pause=True, plot_area=None, show_legend=True, show_grid=False, title='Variable History')

Configure the chart.

Called automatically by salabim during component initialisation.

Parameters:

Name Type Description Default
select list of str

Subset of labels to plot. If None, all tracked variables are shown.

None
pause bool

If True, call plt.pause() after each update, by default True.

True
plot_area any

Streamlit st.empty() placeholder, by default None.

None
show_legend bool

Whether to display the plot legend, by default True.

True
show_grid bool

Whether to display the plot grid, by default False.

False
title str

Chart title, by default "Variable History".

'Variable History'
Source code in dissmodel/visualization/chart.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def setup(
    self,
    select: Optional[list[str]] = None,
    pause: bool = True,
    plot_area: Any = None,
    show_legend: bool = True,
    show_grid: bool = False,
    title: str = "Variable History",
) -> None:
    """
    Configure the chart.

    Called automatically by salabim during component initialisation.

    Parameters
    ----------
    select : list of str, optional
        Subset of labels to plot. If ``None``, all tracked variables
        are shown.
    pause : bool, optional
        If ``True``, call ``plt.pause()`` after each update,
        by default ``True``.
    plot_area : any, optional
        Streamlit ``st.empty()`` placeholder, by default ``None``.
    show_legend : bool, optional
        Whether to display the plot legend, by default ``True``.
    show_grid : bool, optional
        Whether to display the plot grid, by default ``False``.
    title : str, optional
        Chart title, by default ``"Variable History"``.
    """
    self.select = select
    self.interval = 1
    self.time_points = []
    self.pause = pause
    self.plot_area = plot_area
    self.show_legend = show_legend
    self.show_grid = show_grid
    self.title = title

    if not is_notebook():
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlabel("Time")
        self.ax.set_title(self.title)

dissmodel.visualization.map.Map

Bases: Model

Simulation model that renders a live choropleth map.

Extends :class:~dissmodel.core.Model and redraws the map at every time step. Supports three rendering targets:

  • Streamlit — pass a plot_area (st.empty()).
  • Jupyter — detected automatically via :func:is_notebook.
  • Matplotlib window — fallback for plain Python scripts.

Parameters:

Name Type Description Default
gdf GeoDataFrame

GeoDataFrame to render.

required
plot_params dict

Keyword arguments forwarded to :meth:GeoDataFrame.plot (e.g. column, cmap, legend).

required
pause bool

If True, call plt.pause() after each update, by default True. Required for live updates outside notebooks.

required
plot_area any

Streamlit st.empty() placeholder, by default None.

required

Examples:

>>> env = Environment(end_time=10)
>>> Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"})
>>> env.run()
Source code in dissmodel/visualization/map.py
 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
132
133
134
135
136
class Map(Model):
    """
    Simulation model that renders a live choropleth map.

    Extends :class:`~dissmodel.core.Model` and redraws the map at every
    time step. Supports three rendering targets:

    - **Streamlit** — pass a ``plot_area`` (``st.empty()``).
    - **Jupyter** — detected automatically via :func:`is_notebook`.
    - **Matplotlib window** — fallback for plain Python scripts.

    Parameters
    ----------
    gdf : geopandas.GeoDataFrame
        GeoDataFrame to render.
    plot_params : dict
        Keyword arguments forwarded to :meth:`GeoDataFrame.plot`
        (e.g. ``column``, ``cmap``, ``legend``).
    pause : bool, optional
        If ``True``, call ``plt.pause()`` after each update, by default
        ``True``. Required for live updates outside notebooks.
    plot_area : any, optional
        Streamlit ``st.empty()`` placeholder, by default ``None``.

    Examples
    --------
    >>> env = Environment(end_time=10)
    >>> Map(gdf=grid, plot_params={"column": "state", "cmap": "viridis"})
    >>> env.run()
    """

    fig: matplotlib.figure.Figure
    ax: matplotlib.axes.Axes
    gdf: gpd.GeoDataFrame
    plot_params: dict[str, Any]
    pause: bool
    plot_area: Any

    def setup(
        self,
        gdf: gpd.GeoDataFrame,
        plot_params: dict[str, Any],
        pause: bool = True,
        plot_area: Any = None,
    ) -> None:
        """
        Configure the map.

        Called automatically by salabim during component initialisation.

        Parameters
        ----------
        gdf : geopandas.GeoDataFrame
            GeoDataFrame to render.
        plot_params : dict
            Keyword arguments forwarded to :meth:`GeoDataFrame.plot`
            (e.g. ``column``, ``cmap``, ``legend``).
        pause : bool, optional
            If ``True``, call ``plt.pause()`` after each update,
            by default ``True``.
        plot_area : any, optional
            Streamlit ``st.empty()`` placeholder, by default ``None``.
        """
        self.gdf = gdf
        self.plot_params = plot_params
        self.pause = pause
        self.plot_area = plot_area

        if not is_notebook():
            self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6))

    def update(self, year: float, gdf: gpd.GeoDataFrame) -> None:
        """
        Redraw the map for a given simulation time.

        Parameters
        ----------
        year : float
            Current simulation time, displayed in the map title.
        gdf : geopandas.GeoDataFrame
            GeoDataFrame snapshot to render.

        Raises
        ------
        RuntimeError
            If no interactive matplotlib backend is detected and the code is
            not running in a notebook or Streamlit context.
        """
        if is_notebook():
            from IPython.display import clear_output, display
            clear_output(wait=True)
            self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6))
        else:
            self.fig.clf()
            self.ax = self.fig.add_subplot(1, 1, 1)

        gdf.plot(ax=self.ax, **self.plot_params)
        self.ax.set_title(f"Map — Step {year}")
        plt.tight_layout()
        plt.draw()

        if self.plot_area is not None:
            self.plot_area.pyplot(self.fig)
        elif is_notebook():
            from IPython.display import display
            display(self.fig)
            plt.close(self.fig)
        elif self.pause:
            if is_interactive_backend():
                plt.pause(0.01)
                if self.env.now() == self.env.end_time:
                    plt.show()
            else:
                raise RuntimeError(
                    "No interactive matplotlib backend detected. "
                    "On Linux, install tkinter:\n\n"
                    "    sudo apt install python3-tk\n"
                )


    def execute(self) -> None:
        """Redraw the map for the current simulation time step."""
        self.update(year=self.env.now(), gdf=self.gdf)

execute()

Redraw the map for the current simulation time step.

Source code in dissmodel/visualization/map.py
134
135
136
def execute(self) -> None:
    """Redraw the map for the current simulation time step."""
    self.update(year=self.env.now(), gdf=self.gdf)

setup(gdf, plot_params, pause=True, plot_area=None)

Configure the map.

Called automatically by salabim during component initialisation.

Parameters:

Name Type Description Default
gdf GeoDataFrame

GeoDataFrame to render.

required
plot_params dict

Keyword arguments forwarded to :meth:GeoDataFrame.plot (e.g. column, cmap, legend).

required
pause bool

If True, call plt.pause() after each update, by default True.

True
plot_area any

Streamlit st.empty() placeholder, by default None.

None
Source code in dissmodel/visualization/map.py
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
def setup(
    self,
    gdf: gpd.GeoDataFrame,
    plot_params: dict[str, Any],
    pause: bool = True,
    plot_area: Any = None,
) -> None:
    """
    Configure the map.

    Called automatically by salabim during component initialisation.

    Parameters
    ----------
    gdf : geopandas.GeoDataFrame
        GeoDataFrame to render.
    plot_params : dict
        Keyword arguments forwarded to :meth:`GeoDataFrame.plot`
        (e.g. ``column``, ``cmap``, ``legend``).
    pause : bool, optional
        If ``True``, call ``plt.pause()`` after each update,
        by default ``True``.
    plot_area : any, optional
        Streamlit ``st.empty()`` placeholder, by default ``None``.
    """
    self.gdf = gdf
    self.plot_params = plot_params
    self.pause = pause
    self.plot_area = plot_area

    if not is_notebook():
        self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6))

update(year, gdf)

Redraw the map for a given simulation time.

Parameters:

Name Type Description Default
year float

Current simulation time, displayed in the map title.

required
gdf GeoDataFrame

GeoDataFrame snapshot to render.

required

Raises:

Type Description
RuntimeError

If no interactive matplotlib backend is detected and the code is not running in a notebook or Streamlit context.

Source code in dissmodel/visualization/map.py
 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
def update(self, year: float, gdf: gpd.GeoDataFrame) -> None:
    """
    Redraw the map for a given simulation time.

    Parameters
    ----------
    year : float
        Current simulation time, displayed in the map title.
    gdf : geopandas.GeoDataFrame
        GeoDataFrame snapshot to render.

    Raises
    ------
    RuntimeError
        If no interactive matplotlib backend is detected and the code is
        not running in a notebook or Streamlit context.
    """
    if is_notebook():
        from IPython.display import clear_output, display
        clear_output(wait=True)
        self.fig, self.ax = plt.subplots(1, 1, figsize=(10, 6))
    else:
        self.fig.clf()
        self.ax = self.fig.add_subplot(1, 1, 1)

    gdf.plot(ax=self.ax, **self.plot_params)
    self.ax.set_title(f"Map — Step {year}")
    plt.tight_layout()
    plt.draw()

    if self.plot_area is not None:
        self.plot_area.pyplot(self.fig)
    elif is_notebook():
        from IPython.display import display
        display(self.fig)
        plt.close(self.fig)
    elif self.pause:
        if is_interactive_backend():
            plt.pause(0.01)
            if self.env.now() == self.env.end_time:
                plt.show()
        else:
            raise RuntimeError(
                "No interactive matplotlib backend detected. "
                "On Linux, install tkinter:\n\n"
                "    sudo apt install python3-tk\n"
            )

dissmodel.visualization.widgets.display_inputs(obj, st)

Render Streamlit input widgets for every annotated attribute on obj.

Iterates over obj.__annotations__ and creates an appropriate widget for each attribute based on its current value type. The attribute is updated in-place on obj after each interaction.

Widget mapping:

  • boolst.checkbox (checked before int — bool subclasses int)
  • intst.slider (0 – 1000)
  • floatst.slider (0.0 – 1.0, step 0.01)
  • anything else → st.text_input

Parameters:

Name Type Description Default
obj any

Any object with __annotations__ and matching instance attributes, typically a :class:~dissmodel.core.Model subclass.

required
st any

Streamlit module or sidebar object (e.g. st or st.sidebar). Passed as an argument to avoid a hard dependency on Streamlit at import time.

required

Examples:

>>> display_inputs(sir_model, st.sidebar)
>>> display_inputs(ca_model, st)
Source code in dissmodel/visualization/widgets.py
 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
def display_inputs(obj: Any, st: Any) -> None:
    """
    Render Streamlit input widgets for every annotated attribute on ``obj``.

    Iterates over ``obj.__annotations__`` and creates an appropriate widget
    for each attribute based on its current value type. The attribute is
    updated in-place on ``obj`` after each interaction.

    Widget mapping:

    - ``bool``  → ``st.checkbox``  *(checked before int — bool subclasses int)*
    - ``int``   → ``st.slider`` (0 – 1000)
    - ``float`` → ``st.slider`` (0.0 – 1.0, step 0.01)
    - anything else → ``st.text_input``

    Parameters
    ----------
    obj : any
        Any object with ``__annotations__`` and matching instance attributes,
        typically a :class:`~dissmodel.core.Model` subclass.
    st : any
        Streamlit module or sidebar object (e.g. ``st`` or ``st.sidebar``).
        Passed as an argument to avoid a hard dependency on Streamlit at
        import time.

    Examples
    --------
    >>> display_inputs(sir_model, st.sidebar)
    >>> display_inputs(ca_model, st)
    """
    annotations: dict[str, Any] = getattr(obj, "__annotations__", {})

    for name in annotations:
        value: Any = getattr(obj, name, None)

        if isinstance(value, bool):
            # bool must come before int — bool is a subclass of int in Python
            new_value = st.checkbox(name, value=value)
        elif isinstance(value, int):
            new_value = st.slider(name, 0, 1000, value)
        elif isinstance(value, float):
            new_value = st.slider(name, 0.0, 1.0, value, step=0.01)
        else:
            new_value = st.text_input(name, str(value))

        setattr(obj, name, new_value)

dissmodel.visualization.track_plot(label, color, plot_type='line')

Class decorator that registers an attribute for live plotting.

Parameters:

Name Type Description Default
label str

Display label and lookup key for the tracked attribute.

required
color str

Matplotlib-compatible color string (e.g. "red", "#ff0000").

required
plot_type str

Plot style, by default "line".

'line'

Returns:

Type Description
type

The decorated class with _plot_info populated.

Examples:

>>> @track_plot(label="Infected", color="red")
... class SIR(Model):
...     infected: int = 0
Source code in dissmodel/visualization/chart.py
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
def track_plot(
    label: str,
    color: str,
    plot_type: str = "line",
) -> Any:
    """
    Class decorator that registers an attribute for live plotting.

    Parameters
    ----------
    label : str
        Display label and lookup key for the tracked attribute.
    color : str
        Matplotlib-compatible color string (e.g. ``"red"``, ``"#ff0000"``).
    plot_type : str, optional
        Plot style, by default ``"line"``.

    Returns
    -------
    type
        The decorated class with ``_plot_info`` populated.

    Examples
    --------
    >>> @track_plot(label="Infected", color="red")
    ... class SIR(Model):
    ...     infected: int = 0
    """
    def decorator(cls: type) -> type:
        if not hasattr(cls, "_plot_info"):
            cls._plot_info = {}  # type: ignore[attr-defined]
        cls._plot_info[label.lower()] = {  # type: ignore[attr-defined]
            "plot_type": plot_type,
            "label": label,
            "color": color,
            "data": [],
        }
        return cls
    return decorator