From e71d4026a8f6ae7af65fdd042a1e7691712abc7c Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Thu, 15 Aug 2024 12:39:00 -0400 Subject: [PATCH 1/8] add shape options in matplotlib visualization "shape" in agent_portrayal corresponds to "marker" in matplotlib's scatter function. --- mesa/visualization/components/matplotlib.py | 45 ++++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index aadfa206472..b8d24467034 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -2,6 +2,7 @@ import solara from matplotlib.figure import Figure from matplotlib.ticker import MaxNLocator +from collections import defaultdict import mesa @@ -23,12 +24,43 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = Non solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies) +# matplotlib scatter does not allow for multiple shapes in one call +def _split_and_scatter(portray_data, space_ax): + grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []}) + + if "marker" not in portray_data: + # standard scatter is fine, default marker + space_ax.scatter(**portray_data) + return + + # Extract data from the dictionary + markers = portray_data["marker"] + x = portray_data["x"] + y = portray_data["y"] + s = portray_data["s"] + c = portray_data["c"] + + # Group the data by marker + for i in range(len(x)): + marker = markers[i] + grouped_data[marker]["x"].append(x[i]) + grouped_data[marker]["y"].append(y[i]) + grouped_data[marker]["s"].append(s[i]) + grouped_data[marker]["c"].append(c[i]) + + # Plot each group with the same marker + for marker, data in grouped_data.items(): + space_ax.scatter(data["x"], data["y"], s=data["s"], + c=data["c"], marker=marker) + + def _draw_grid(space, space_ax, agent_portrayal): def portray(g): x = [] y = [] s = [] # size c = [] # color + marker = [] # shape for i in range(g.width): for j in range(g.height): content = g._grid[i][j] @@ -45,6 +77,8 @@ def portray(g): s.append(data["size"]) if "color" in data: c.append(data["color"]) + if "shape" in data: + marker.append(data["shape"]) out = {"x": x, "y": y} # This is the default value for the marker size, which auto-scales # according to the grid area. @@ -53,11 +87,13 @@ def portray(g): out["s"] = s if len(c) > 0: out["c"] = c + if len(marker) > 0: + out["marker"] = marker return out space_ax.set_xlim(-1, space.width) space_ax.set_ylim(-1, space.height) - space_ax.scatter(**portray(space)) + _split_and_scatter(portray(space), space_ax) def _draw_network_grid(space, space_ax, agent_portrayal): @@ -77,6 +113,7 @@ def portray(space): y = [] s = [] # size c = [] # color + marker = [] # shape for agent in space._agent_to_index: data = agent_portrayal(agent) _x, _y = agent.pos @@ -86,11 +123,15 @@ def portray(space): s.append(data["size"]) if "color" in data: c.append(data["color"]) + if "shape" in data: + marker.append(data["shape"]) out = {"x": x, "y": y} if len(s) > 0: out["s"] = s if len(c) > 0: out["c"] = c + if len(marker) > 0: + out["marker"] = marker return out # Determine border style based on space.torus @@ -110,7 +151,7 @@ def portray(space): space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding) # Portray and scatter the agents in the space - space_ax.scatter(**portray(space)) + _split_and_scatter(portray(space), space_ax) @solara.component From 7da6940ef7eadec796473dd6a4032f70b185a667 Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Thu, 15 Aug 2024 12:39:40 -0400 Subject: [PATCH 2/8] fix code formatting --- mesa/visualization/components/matplotlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index b8d24467034..bf9d8b284dd 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -1,8 +1,9 @@ +from collections import defaultdict + import networkx as nx import solara from matplotlib.figure import Figure from matplotlib.ticker import MaxNLocator -from collections import defaultdict import mesa @@ -50,8 +51,7 @@ def _split_and_scatter(portray_data, space_ax): # Plot each group with the same marker for marker, data in grouped_data.items(): - space_ax.scatter(data["x"], data["y"], s=data["s"], - c=data["c"], marker=marker) + space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker) def _draw_grid(space, space_ax, agent_portrayal): From 536f8a710ca29c73223201bda0728b065c68a46d Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Thu, 15 Aug 2024 16:27:10 -0400 Subject: [PATCH 3/8] added default values to viz Previously, if only some agent_portrayals provided info like size, color, matplotlib would not accept the parameters and the program would crash. Additionally, edited some names for coherence. --- mesa/visualization/components/matplotlib.py | 67 +++++++++------------ 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index bf9d8b284dd..b4d8fc84827 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -29,21 +29,18 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = Non def _split_and_scatter(portray_data, space_ax): grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []}) - if "marker" not in portray_data: - # standard scatter is fine, default marker - space_ax.scatter(**portray_data) - return - # Extract data from the dictionary - markers = portray_data["marker"] x = portray_data["x"] y = portray_data["y"] s = portray_data["s"] c = portray_data["c"] + m = portray_data["m"] + + assert len(x) == len(y) == len(s) == len(c) == len(m) # Group the data by marker for i in range(len(x)): - marker = markers[i] + marker = m[i] grouped_data[marker]["x"].append(x[i]) grouped_data[marker]["y"].append(y[i]) grouped_data[marker]["s"].append(s[i]) @@ -60,7 +57,7 @@ def portray(g): y = [] s = [] # size c = [] # color - marker = [] # shape + m = [] # shape for i in range(g.width): for j in range(g.height): content = g._grid[i][j] @@ -73,22 +70,18 @@ def portray(g): data = agent_portrayal(agent) x.append(i) y.append(j) - if "size" in data: - s.append(data["size"]) - if "color" in data: - c.append(data["color"]) - if "shape" in data: - marker.append(data["shape"]) - out = {"x": x, "y": y} - # This is the default value for the marker size, which auto-scales - # according to the grid area. - out["s"] = (180 / max(g.width, g.height)) ** 2 - if len(s) > 0: - out["s"] = s - if len(c) > 0: - out["c"] = c - if len(marker) > 0: - out["marker"] = marker + + # This is the default value for the marker size, which auto-scales + # according to the grid area. + default_size = (180 / max(g.width, g.height)) ** 2 + # establishing a default prevents misalignment if some agents are not given size, color, etc. + size = data.get("size", default_size) + s.append(size) + color = data.get("color", "b") + c.append(color) + mark = data.get("shape", ".") + m.append(mark) + out = {"x": x, "y": y, "s": s, "c": c, "m": m} return out space_ax.set_xlim(-1, space.width) @@ -113,25 +106,23 @@ def portray(space): y = [] s = [] # size c = [] # color - marker = [] # shape + m = [] # shape for agent in space._agent_to_index: data = agent_portrayal(agent) _x, _y = agent.pos x.append(_x) y.append(_y) - if "size" in data: - s.append(data["size"]) - if "color" in data: - c.append(data["color"]) - if "shape" in data: - marker.append(data["shape"]) - out = {"x": x, "y": y} - if len(s) > 0: - out["s"] = s - if len(c) > 0: - out["c"] = c - if len(marker) > 0: - out["marker"] = marker + + # This is matplotlib's default marker size + default_size = 20 + # establishing a default prevents misalignment if some agents are not given size, color, etc. + size = data.get("size", default_size) + s.append(size) + color = data.get("color", "b") + c.append(color) + mark = data.get("shape", ".") + m.append(mark) + out = {"x": x, "y": y, "s": s, "c": c, "m": m} return out # Determine border style based on space.torus From 6e39e2f1057b50fbce6950349a59dd6ad6267053 Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Thu, 15 Aug 2024 16:35:08 -0400 Subject: [PATCH 4/8] fix assertion to exception --- mesa/visualization/components/matplotlib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index b4d8fc84827..2171bb55a64 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -36,7 +36,12 @@ def _split_and_scatter(portray_data, space_ax): c = portray_data["c"] m = portray_data["m"] - assert len(x) == len(y) == len(s) == len(c) == len(m) + if not (len(x) == len(y) == len(s) == len(c) == len(m)): + raise ValueError( + "Length mismatch in portrayal data lists: " + f"x: {len(x)}, y: {len(y)}, s: {len(s)}, " + f"c: {len(c)}, m: {len(m)}" + ) # Group the data by marker for i in range(len(x)): From 1c651647184c1d1022571ca9a435fd5c3d0d85da Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Fri, 16 Aug 2024 10:23:31 -0400 Subject: [PATCH 5/8] fix error message, default shape, color error message shows public names of visualization attributes. default color and marker fixed to "tab:blue" & "o" respectively --- mesa/visualization/components/matplotlib.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index 2171bb55a64..77c55a379a2 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -39,8 +39,8 @@ def _split_and_scatter(portray_data, space_ax): if not (len(x) == len(y) == len(s) == len(c) == len(m)): raise ValueError( "Length mismatch in portrayal data lists: " - f"x: {len(x)}, y: {len(y)}, s: {len(s)}, " - f"c: {len(c)}, m: {len(m)}" + f"x: {len(x)}, y: {len(y)}, size: {len(s)}, " + f"color: {len(c)}, marker: {len(m)}" ) # Group the data by marker @@ -53,7 +53,8 @@ def _split_and_scatter(portray_data, space_ax): # Plot each group with the same marker for marker, data in grouped_data.items(): - space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker) + space_ax.scatter(data["x"], data["y"], s=data["s"], + c=data["c"], marker=marker) def _draw_grid(space, space_ax, agent_portrayal): @@ -82,9 +83,9 @@ def portray(g): # establishing a default prevents misalignment if some agents are not given size, color, etc. size = data.get("size", default_size) s.append(size) - color = data.get("color", "b") + color = data.get("color", "tab:blue") c.append(color) - mark = data.get("shape", ".") + mark = data.get("shape", "o") m.append(mark) out = {"x": x, "y": y, "s": s, "c": c, "m": m} return out @@ -123,9 +124,9 @@ def portray(space): # establishing a default prevents misalignment if some agents are not given size, color, etc. size = data.get("size", default_size) s.append(size) - color = data.get("color", "b") + color = data.get("color", "tab:blue") c.append(color) - mark = data.get("shape", ".") + mark = data.get("shape", "o") m.append(mark) out = {"x": x, "y": y, "s": s, "c": c, "m": m} return out From adf9beefd312d68befa621b457d1cd192b9c31a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:24:44 +0000 Subject: [PATCH 6/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/visualization/components/matplotlib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py index 77c55a379a2..83d0e3d8eaf 100644 --- a/mesa/visualization/components/matplotlib.py +++ b/mesa/visualization/components/matplotlib.py @@ -53,8 +53,7 @@ def _split_and_scatter(portray_data, space_ax): # Plot each group with the same marker for marker, data in grouped_data.items(): - space_ax.scatter(data["x"], data["y"], s=data["s"], - c=data["c"], marker=marker) + space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker) def _draw_grid(space, space_ax, agent_portrayal): From 09bb236e6745306cfdf0689f6c34a03dc886a4b9 Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Mon, 19 Aug 2024 18:29:22 -0400 Subject: [PATCH 7/8] Update vizualization tutorial for shape mention Viz tutorial now mentions that in addition to size and color, default drawer allows for custom shape. --- docs/tutorials/visualization_tutorial.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb index 14d9ba4d1ea..ba8d7a922d2 100644 --- a/docs/tutorials/visualization_tutorial.ipynb +++ b/docs/tutorials/visualization_tutorial.ipynb @@ -142,7 +142,8 @@ "source": [ "#### Changing the agents\n", "\n", - "In the visualization above, all we could see is the agents moving around -- but not how much money they had, or anything else of interest. Let's change it so that agents who are broke (wealth 0) are drawn in red, smaller. (TODO: currently, we can't predict the drawing order of the circles, so a broke agent may be overshadowed by a wealthy agent. We should fix this by doing a hollow circle instead)\n", + "In the visualization above, all we could see is the agents moving around -- but not how much money they had, or anything else of interest. Let's change it so that agents who are broke (wealth 0) are drawn in red, smaller. (TODO: Currently, we can't predict the drawing order of the circles, so a broke agent may be overshadowed by a wealthy agent. We should fix this by doing a hollow circle instead)\n", + "In addition to size and color, an agent's shape can also be customized when using the default drawer. The allowed values for shapes can be found [here](https://matplotlib.org/stable/api/markers_api.html).\n", "\n", "To do this, we go back to our `agent_portrayal` code and add some code to change the portrayal based on the agent properties and launch the server again." ] From 80727913404b3ce05a7eb46925318e6b6337f389 Mon Sep 17 00:00:00 2001 From: Robert Hopkins Date: Mon, 19 Aug 2024 18:41:16 -0400 Subject: [PATCH 8/8] Docstring mentions customization options --- mesa/visualization/solara_viz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 4f1598846e5..375cbce8fb8 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -103,7 +103,8 @@ def SolaraViz( model_params: Parameters for initializing the model measures: List of callables or data attributes to plot name: Name for display - agent_portrayal: Options for rendering agents (dictionary) + agent_portrayal: Options for rendering agents (dictionary); + Default drawer supports custom `"size"`, `"color"`, and `"shape"`. space_drawer: Method to render the agent space for the model; default implementation is the `SpaceMatplotlib` component; simulations with no space to visualize should