Custom Plotting
Village provides three plot classes you can subclass to add custom visualizations. Each one is triggered at a different point in the data lifecycle:
Class |
When it runs |
Data available |
|---|---|---|
|
After every trial, during the session |
Current session trials so far |
|
When a finished session is selected in the GUI |
Full session CSV |
|
When a subject is selected in the GUI |
All sessions for that subject |
Create a file anywhere inside your project’s code directory and define a subclass
with the exact class name shown below. The system detects it automatically at startup
and uses it instead of the default.
OnlinePlotBase
OnlinePlotBase lets you monitor the session in real time. The plot is embedded in
the GUI and updated after every trial.
Override two methods:
create_figure_and_axes()— called once when the figure is first needed. Createself.figand any axes you need (store them as instance attributes).update_plot(df)— called after each trial with the growing session DataFrame. Clear and redraw your axes here.
Note
Do not call plt.show() or fig.savefig() — the figure is managed by the GUI.
Always clear the axes at the start of update_plot to avoid stacking traces.
df columns (one row per completed trial, grows after each trial):
Fixed columns (always present)
trial— trial number (1-based integer).date— session start timestamp string.subject— subject name.task— task class name.system_name— name of the Village system.run_mode—"Manual"or"Automatic".session— session index within the subject’s history.
Bpod event columns (present when using a Bpod controller)
TRIAL_START— time (s) when the trial state machine started.One column per Bpod event and state (e.g.
Port1In,Port1Out,Tup, …). Values are timestamps in seconds;NaNif the event did not occur in that trial.
Custom columns
Any value registered with self.register_value(name, value) inside after_trial
appears as an additional column.
Example — plot accuracy and reaction time trial by trial:
from village.custom_classes.online_plot_base import OnlinePlotBase
class OnlinePlot(OnlinePlotBase):
def create_figure_and_axes(self) -> None:
import matplotlib.pyplot as plt
self.fig, (self.ax_acc, self.ax_rt) = plt.subplots(
2, 1, figsize=(10, 6), sharex=True
)
self.fig.tight_layout(pad=2)
def update_plot(self, df) -> None:
self.ax_acc.clear()
self.ax_rt.clear()
if df.empty or "correct" not in df.columns:
return
self.ax_acc.plot(df["trial"], df["correct"].rolling(10).mean(), color="steelblue")
self.ax_acc.set_ylabel("Accuracy (10-trial rolling mean)")
self.ax_acc.set_ylim(0, 1)
if "response_time" in df.columns:
self.ax_rt.plot(df["trial"], df["response_time"], "o", color="salmon", ms=4)
self.ax_rt.set_ylabel("Response time (s)")
self.ax_rt.set_xlabel("Trial")
SessionPlotBase
SessionPlotBase produces a static figure shown in the GUI when the user clicks on
a finished session in the Sessions table.
Override create_plot(df, weight, width, height) and return a matplotlib.figure.Figure.
Arguments:
df— the full session CSV as a DataFrame. Same columns asOnlinePlotBase.df(see above), with all trials already present.weight— body weight of the subject at the time of the session (float, grams).0.0if not recorded.width,height— figure size in inches requested by the GUI.
Example — plot trial outcome over time with body weight in the title:
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from village.custom_classes.session_plot_base import SessionPlotBase
class SessionPlot(SessionPlotBase):
def create_plot(self, df, weight, width=10, height=8) -> Figure:
fig, ax = plt.subplots(figsize=(width, height))
if "correct" in df.columns:
ax.plot(df["trial"], df["correct"].rolling(10).mean(), color="steelblue")
ax.set_ylabel("Accuracy (10-trial rolling mean)")
ax.set_ylim(0, 1)
title = f"Session — {df['task'].iloc[0]}"
if weight:
title += f" | weight: {weight:.1f} g"
ax.set_title(title)
ax.set_xlabel("Trial")
return fig
SubjectPlotBase
SubjectPlotBase produces a static figure shown when the user clicks on a subject
in the Subjects table. It has access to the subject’s full history across all sessions.
Override create_plot(df, summary_df, width, height) and return a matplotlib.figure.Figure.
Arguments:
df— all trials for this subject, concatenated across sessions. Same trial-level columns as above, plus asessioncolumn (integer, 1-based) that identifies which session each trial belongs to.summary_df— one row per session for this subject, fromsessions_summary.csv. Columns:Column
Description
dateSession start timestamp
subjectSubject name
tagRFID tag
weightBody weight (g) at session start
taskTask name
durationSession duration in seconds
trialsNumber of completed trials
waterWater delivered (µL)
settingsJSON string of the settings used
width,height— figure size in inches.
Example — accuracy per session and weight evolution:
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from village.custom_classes.subject_plot_base import SubjectPlotBase
class SubjectPlot(SubjectPlotBase):
def create_plot(self, df, summary_df, width=10, height=8) -> Figure:
fig, (ax_acc, ax_w) = plt.subplots(2, 1, figsize=(width, height), sharex=True)
if "correct" in df.columns:
acc = df.groupby("session")["correct"].mean()
ax_acc.plot(acc.index, acc.values, "o-", color="steelblue")
ax_acc.set_ylabel("Mean accuracy per session")
ax_acc.set_ylim(0, 1)
if not summary_df.empty and "weight" in summary_df.columns:
ax_w.plot(
range(1, len(summary_df) + 1),
summary_df["weight"].values,
"s-",
color="salmon",
)
ax_w.set_ylabel("Weight (g)")
ax_w.set_xlabel("Session")
fig.suptitle(df["subject"].iloc[0])
fig.tight_layout()
return fig