Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Functions/+BpodLib/+ui/+utils/setDarkMode.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
function setDarkMode(ax)
% Apply dark mode styling to axes and its children
% setDarkMode(ax) configures the axes with a dark color scheme
%
% Input:
% ax - Axes handle or figure handle to modify

% Define colors
dark_bg = [0.1 0.1 0.1]; % Dark background
light_text = [0.9 0.9 0.9]; % Light text/line color
grid_color = [0.3 0.3 0.3]; % Subdued grid color

% Check if input is figure or axes
if strcmp(get(ax,'Type'), 'figure')
fig = ax;
ax = findobj(fig, 'Type', 'axes');
else
fig = ancestor(ax, 'figure');
end

% Set figure properties
set(fig, ...
'Color', dark_bg, ...
'InvertHardcopy', 'off'); % Preserve colors when printing

% Set axes properties
set(ax, ...
'Color', dark_bg, ...
'XColor', light_text, ...
'YColor', light_text, ...
'ZColor', light_text, ...
'GridColor', grid_color, ...
'MinorGridColor', grid_color, ...
'GridAlpha', 0.5, ...
'MinorGridAlpha', 0.2);

% Set title and labels
title_handle = get(ax, 'Title');
xlabel_handle = get(ax, 'XLabel');
ylabel_handle = get(ax, 'YLabel');
zlabel_handle = get(ax, 'ZLabel');

set([title_handle, xlabel_handle, ylabel_handle, zlabel_handle], ...
'Color', light_text);

% todo: this modifies the patchs behind axes
% Modify all children (lines, patches, etc.)
% children = findobj(ax, '-property', 'Color');
% for i = 1:length(children)
% try
% % Skip if it's a histogram (special case)
% if ~isa(children(i), 'matlab.graphics.chart.primitive.Histogram')
% set(children(i), 'Color', light_text);
% end
% catch
% continue
% end
% end

% Set colormap to something that works well with dark background
colormap(ax, 'parula'); % or 'viridis', 'plasma'

% If there's a legend, update its colors
leg = findobj(fig, 'Type', 'legend');
if ~isempty(leg)
set(leg, ...
'TextColor', light_text, ...
'Color', dark_bg, ...
'EdgeColor', grid_color);
end

% If there's a colorbar, update it
cb = findobj(fig, 'Type', 'colorbar');
if ~isempty(cb)
set(cb, ...
'Color', light_text, ...
'YColor', light_text, ...
'XColor', light_text);
end
end
45 changes: 45 additions & 0 deletions Functions/+BpodLib/+vis/+lick/calculateStateLineData.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function [times, states] = calculateStateLineData(portins, portouts, endtime)
% Build plot() ready lick line data
% [times, states] = calculateStateLineData(portins, portouts, endtime)
%
% portins and portouts are expected to be the output of cleanPortTimes()
% plot(times, states) will generate a line in up state during lick
%
% Arguments
% ---------
% portins : double
% Port in times
% portouts : double
% Port out times, numel() same as portins
% endtime : double
% A single time to draw the end of the line to
%
% Returns
% -------
% times : double
% Times of events
% states : double
% State of port (1 or 0)

% Add 1 for licking, 0 for not licking
data = [[ones(size(portins)) zeros(size(portouts))] ; [portins, portouts]];

% Sort port events according to time
[~, idx] = sort(data(2,:));
data = data(:,idx);

% If no lick literally at start of trial:
% The opposite state to the first recorded was at time 0
if data(2,1) ~= 0 % checks if there's event at time 0
data = [[mod(1,data(1,1));0], data];
end

% The end state continues to the endtime
% e.g. if the portout never happens then the state remained in/high
if data(2,end) ~= endtime
data = [data [data(1,end); endtime]];
end

times = data(2, :);
states = data(1, :);
end
24 changes: 24 additions & 0 deletions Functions/+BpodLib/+vis/+lick/cleanPortTimes.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function [portins, portouts] = cleanPortTimes(portins, portouts)
% Cleanse port data
% [portins, portouts] = cleanPortTimes(portins, portouts)
%
%

if ~isempty(portins)
if isempty(portouts)
portouts = NaN;
end

% Handle edge cases
if portins(1) > portouts(1) % an out before an in
portins = [NaN portins];
end
if portins(end) > portouts(end) % in before trial end
portouts(end + 1) = NaN;
end
elseif ~isempty(portouts)
portins = NaN;
else
portins = [];
portouts = [];
end
1 change: 1 addition & 0 deletions Functions/@BpodObject/BpodObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
LastStateMatrix % Last state matrix completed. This is updated each time a trial run completes.
HardwareState % Current state of I/O lines and serial codes
StateMachineInfo % Struct with information about state machines (customized for connected hardware)
StateHandlerFunction % function handle to run
GUIHandles % Struct with graphics handles
GUIData % Struct with graphics data
InputsEnabled % Struct storing input channels that are connected to something. This is modified from the settings menu UI.
Expand Down
1 change: 1 addition & 0 deletions Functions/Internal Functions/RunBpodEmulator.m
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@

% Evaluate state timer transitions
timeInState = BpodSystem.Emulator.CurrentTime - BpodSystem.Emulator.StateStartTime;
BpodSystem.Emulator.timeInState = timeInState;
stateTimer = BpodSystem.StateMatrix.StateTimers(BpodSystem.Emulator.CurrentState);
if (timeInState > stateTimer) && (BpodSystem.Emulator.MeaningfulTimer(BpodSystem.Emulator.CurrentState) == 1)
BpodSystem.Emulator.nCurrentEvents = BpodSystem.Emulator.nCurrentEvents + 1;
Expand Down
222 changes: 222 additions & 0 deletions Functions/Plugins/Plots/LiveStatePlot.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
% Plugin for drawing a live update of trial
% obj = LiveStatePlot(_)
%
% Arguments
% ---------
% BpodSystem : BpodObject
% BpodSystem, optional (defaults)
%
% Keyword Arguments
% -----------------
% darkmode : bool
% Make the plot dark mode, default true
% autoclose : bool
% Close the figure window at the end of session, default true

%{
----------------------------------------------------------------------------

This file is part of the Sanworks Bpod repository
Copyright (C) Sanworks LLC, Rochester, New York, USA

----------------------------------------------------------------------------

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3.

This program is distributed WITHOUT ANY WARRANTY and without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
%}

classdef LiveStatePlot < handle
properties
GUIHandles
BpodSystem
lastTic % The last time of update
trialStartTic
lastNEvents
timeDiff

settings
maxDrawHz
end
methods
function obj = LiveStatePlot(varargin)
p = inputParser();
p.addOptional('BpodSystem', [])
p.addParameter('darkmode', true)
p.addParameter('autoclose', true)
p.parse(varargin{:})
if isempty(p.Results.BpodSystem)
global BpodSystem
else
BpodSystem = p.Results.BpodSystem;
end
obj.settings = struct();
obj.settings.darkmode = p.Results.darkmode;
obj.settings.defaultMaxTime = [];
obj.settings.leadTime = 3;

% Build axes
obj.BpodSystem = BpodSystem;
obj.GUIHandles.Figure = figure('Name', 'LivePlot', 'MenuBar', 'none', 'NumberTitle', 'off');
if p.Results.autoclose
obj.BpodSystem.ProtocolFigures.LivePlot = obj;
else
obj.BpodSystem.GUIHandles.LivePlot = obj;
end
xpos = 0.2;
obj.GUIHandles.AxesState = axes(obj.GUIHandles.Figure, 'Position', [xpos, .5, 1-xpos-.1, .2]);
obj.GUIHandles.AxesEvent = axes(obj.GUIHandles.Figure, 'Position', [xpos, 0, 1-xpos-.1, .2]);
obj.GUIHandles.PlotState = stairs(obj.GUIHandles.AxesState, 0, 0); % if stairs initialises with [] data then it won't update
obj.GUIHandles.PlotEvent = scatter(obj.GUIHandles.AxesEvent, [], []);
obj.apply_style()

obj.lastTic = tic;
obj.lastNEvents = [];
obj.timeDiff = 0;
obj.maxDrawHz = 60;
end

function apply_style(obj)
% Sets axes and plot styling

xpos = .2;
set(obj.GUIHandles.AxesState,...
'Position', [xpos, .5, 1-xpos, .5]);
set(obj.GUIHandles.AxesEvent,...
'Position', [xpos, .1, 1-xpos, .4]);

% -- Set colors
if obj.settings.darkmode
color = 'w';
else
color = 'k';
end
set(obj.GUIHandles.PlotState,...
'Color', color, ...
'Marker', '.')
set(obj.GUIHandles.PlotEvent,...
'Color', color,...
'Marker', '|')
grid(obj.GUIHandles.AxesState, 'on')
grid(obj.GUIHandles.AxesEvent, 'on')

for ax = {obj.GUIHandles.AxesState, obj.GUIHandles.AxesEvent}
if obj.settings.darkmode
BpodLib.ui.utils.setDarkMode(ax{1})
end
end

% Y labels (states and events) are set in obj.newTrial()

end

function close(obj)
close(obj.GUIHandles.Figure);
end

function update(obj, operation)
switch operation
case 'trial_start'
obj.newTrial()
return
case 'trial_end'
return
end

% Limit rate the plot update
if toc(obj.lastTic) < 1/obj.maxDrawHz
return
end

if ~isvalid(obj.GUIHandles.Figure)
assert(obj.BpodSystem.Status.BeingUsed == 0)
return
end

% Retrieve a timestamp for each recorded event
if obj.BpodSystem.EmulatorMode == 0
eventTimes = obj.BpodSystem.Status.liveEventTimestamps(obj.BpodSystem.Status.liveEventTimestamps ~= 0) / 1000;
else
eventTimes = obj.BpodSystem.Emulator.Timestamps(obj.BpodSystem.Emulator.Timestamps ~= 0);
end

% Calculate the time offset between the machine and computer
currentTime = toc(obj.trialStartTic);
if numel(eventTimes) > obj.lastNEvents % If there is a new event
obj.lastNEvents = numel(eventTimes);
% Calculate difference between machine's time and toc() time
obj.timeDiff = eventTimes(end) - currentTime;
end
currentTime = currentTime + obj.timeDiff;

% Build state index and times
stateindices = obj.BpodSystem.Status.states(obj.BpodSystem.Status.states ~= 0);

stateChangeIndexes = obj.BpodSystem.Status.stateChangeIndexes(obj.BpodSystem.Status.stateChangeIndexes ~= 0);
stateTimes = eventTimes(stateChangeIndexes);

% Build stairs-compatible x/y data with state line continuing to current time
stateTimes = [0 stateTimes currentTime];
stateindices = [stateindices stateindices(end)];

% Update plot
set(obj.GUIHandles.PlotState, ...
'XData', stateTimes, ...
'YData', stateindices);

% Maintain time window limits
if ~isempty(obj.settings.defaultMaxTime)
if (currentTime + obj.settings.leadTime) > obj.settings.defaultMaxTime
xLimMax = currentTime + obj.settings.leadTime;
else
xLimMax = obj.settings.defaultMaxTime;
end
else
xLimMax = currentTime + obj.settings.leadTime;
end
set(obj.GUIHandles.AxesState,...
'XLim', [0, xLimMax]);
obj.update_events(eventTimes, obj.BpodSystem.Status.events(obj.BpodSystem.Status.events ~= 0))

obj.lastTic = tic;
end

function update_events(obj, eventTimes, eventIndexes)
times = eventTimes;
ydata = eventIndexes;
% todo: enable custom specification of focus events
% todo: build pulse traces for port events

set(obj.GUIHandles.PlotEvent,...
'XData', times,...
'YData', ydata)
set(obj.GUIHandles.AxesEvent,...
'XLim', get(obj.GUIHandles.AxesState, 'XLim'));
end

function newTrial(obj)
obj.lastNEvents = 0;
obj.trialStartTic = tic;

% -- Prepare state axes
stateNames = obj.BpodSystem.StateMatrix.StateNames; % initialised only when SendStateMatrix() is used...
set(obj.GUIHandles.AxesState,...
'YTick', 1:numel(stateNames), ...
'YTickLabel', stateNames,...
'YLim', [-0.5, numel(stateNames) + .5])
% obj.timeDiff = % todo: this could be calculated from trialStartTimestamp ?

% -- Prepare event axes
% Determine which events are even valid?

end
end
end

Loading