boris-behav-obs 9.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/plot_events.py ADDED
@@ -0,0 +1,634 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import datetime as dt
24
+ import pathlib as pl
25
+
26
+ import matplotlib
27
+
28
+ matplotlib.use("QtAgg")
29
+
30
+ import matplotlib.dates
31
+
32
+ import matplotlib.pyplot as plt
33
+ import numpy as np
34
+ from matplotlib.dates import (
35
+ DateFormatter,
36
+ )
37
+
38
+ from . import config as cfg
39
+ from . import db_functions, project_functions, observation_operations
40
+ from . import utilities as util
41
+
42
+ # matplotlib.pyplot.switch_backend("Qt5Agg")
43
+
44
+
45
+ def default_value(ethogram, behavior, parameter):
46
+ """
47
+ return value for duration in case of point event
48
+ """
49
+ default_value_ = 0
50
+ if project_functions.event_type(behavior, ethogram) in cfg.POINT_EVENT_TYPES and parameter in ["duration"]:
51
+ default_value_ = "NA"
52
+ return default_value_
53
+
54
+
55
+ def init_behav_modif(
56
+ ethogram: dict,
57
+ selected_subjects: list,
58
+ distinct_behav_modif,
59
+ include_modifiers,
60
+ parameters,
61
+ ) -> dict:
62
+ """
63
+ initialize dictionary with subject, behaviors and modifiers
64
+ """
65
+
66
+ behaviors = {}
67
+ for subj in selected_subjects:
68
+ behaviors[subj] = {}
69
+ for behav_modif in distinct_behav_modif:
70
+ behav, modif = behav_modif
71
+ behav_modif_str = "|".join(behav_modif) if modif else behav
72
+
73
+ if behav_modif_str not in behaviors[subj]:
74
+ behaviors[subj][behav_modif_str] = {}
75
+
76
+ for parameter in parameters:
77
+ behaviors[subj][behav_modif_str][parameter[0]] = default_value(ethogram, behav_modif_str, parameter[0])
78
+
79
+ return behaviors
80
+
81
+
82
+ def init_behav(ethogram: dict, selected_subjects: list, distinct_behaviors, parameters) -> dict:
83
+ """
84
+ initialize dictionary with subject, behaviors and modifiers
85
+ """
86
+
87
+ behaviors: dict = {}
88
+ for subj in selected_subjects:
89
+ behaviors[subj] = {}
90
+ for behavior in distinct_behaviors:
91
+ if behavior not in behaviors[subj]:
92
+ behaviors[subj][behavior] = {}
93
+ for parameter in parameters:
94
+ behaviors[subj][behavior][parameter] = default_value(ethogram, behavior, parameter)
95
+ return behaviors
96
+
97
+
98
+ def create_behaviors_bar_plot(
99
+ pj: dict,
100
+ selected_observations: list,
101
+ param: dict,
102
+ plot_directory: str,
103
+ output_format: str,
104
+ plot_colors: list = cfg.BEHAVIORS_PLOT_COLORS,
105
+ ) -> dict:
106
+ """
107
+ time budget bar plot
108
+
109
+ Args:
110
+ pj (dict): project
111
+ param (dict): parameters
112
+ plot_directory (str): path of directory
113
+ output_format (str): image format
114
+
115
+ Returns:
116
+ dict:
117
+ """
118
+
119
+ selected_subjects = param[cfg.SELECTED_SUBJECTS]
120
+ selected_behaviors = param[cfg.SELECTED_BEHAVIORS]
121
+ start_time = param[cfg.START_TIME]
122
+ end_time = param[cfg.END_TIME]
123
+
124
+ parameters = ["duration", "number of occurences"]
125
+
126
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(pj, selected_subjects, selected_observations, selected_behaviors)
127
+
128
+ if not ok:
129
+ return {"error": True, "message": msg}
130
+
131
+ # extract all behaviors from ethogram for colors in plot
132
+ all_behaviors = util.all_behaviors(pj[cfg.ETHOGRAM])
133
+
134
+ for obs_id in selected_observations:
135
+ cursor = db_connector.cursor()
136
+ # distinct behaviors
137
+ cursor.execute(
138
+ "SELECT distinct behavior FROM aggregated_events WHERE observation = ?",
139
+ (obs_id,),
140
+ )
141
+ distinct_behav = [rows["behavior"] for rows in cursor.fetchall()]
142
+
143
+ # add selected behaviors that are not observed
144
+ """
145
+ if not param[EXCLUDE_BEHAVIORS]:
146
+ for behavior in selected_behaviors:
147
+ if [x for x in distinct_behav if x == behavior] == []:
148
+ distinct_behav.append(behavior)
149
+ """
150
+
151
+ # distinct subjects
152
+ cursor.execute(
153
+ "SELECT distinct subject FROM aggregated_events WHERE observation = ?",
154
+ (obs_id,),
155
+ )
156
+ distinct_subjects = [rows["subject"] for rows in cursor.fetchall()]
157
+
158
+ behaviors = init_behav(pj[cfg.ETHOGRAM], distinct_subjects, distinct_behav, parameters)
159
+
160
+ # plot creation
161
+ if len(distinct_subjects) > 1:
162
+ fig, axs = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
163
+ fig2, axs2 = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
164
+
165
+ else:
166
+ fig, ax = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
167
+ axs = np.ndarray(shape=(1), dtype=type(ax))
168
+ axs[0] = ax
169
+
170
+ fig2, ax2 = plt.subplots(nrows=1, ncols=len(distinct_subjects), sharey=True)
171
+ axs2 = np.ndarray(shape=(1), dtype=type(ax2))
172
+ axs2[0] = ax2
173
+
174
+ fig.suptitle("Durations of behaviors")
175
+ fig2.suptitle("Number of occurences of behaviors")
176
+
177
+ # if modifiers not to be included set modifiers to ""
178
+ cursor.execute("UPDATE aggregated_events SET modifiers = ''")
179
+
180
+ # time
181
+ obs_length = observation_operations.observation_total_length(pj[cfg.OBSERVATIONS][obs_id])
182
+ if obs_length == -1:
183
+ obs_length = 0
184
+
185
+ if param["time"] == cfg.TIME_FULL_OBS:
186
+ min_time = float(0)
187
+ max_time = float(obs_length)
188
+
189
+ if param["time"] == cfg.TIME_EVENTS:
190
+ try:
191
+ min_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0])
192
+ except Exception:
193
+ min_time = float(0)
194
+ try:
195
+ max_time = float(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0])
196
+ except Exception:
197
+ max_time = float(obs_length)
198
+
199
+ if param["time"] in (cfg.TIME_ARBITRARY_INTERVAL, cfg.TIME_OBS_INTERVAL):
200
+ min_time = float(start_time)
201
+ max_time = float(end_time)
202
+
203
+ cursor.execute(
204
+ "UPDATE aggregated_events SET start = ? WHERE observation = ? AND start < ? AND stop BETWEEN ? AND ?",
205
+ (
206
+ min_time,
207
+ obs_id,
208
+ min_time,
209
+ min_time,
210
+ max_time,
211
+ ),
212
+ )
213
+ cursor.execute(
214
+ "UPDATE aggregated_events SET stop = ? WHERE observation = ? AND stop > ? AND start BETWEEN ? AND ?",
215
+ (
216
+ max_time,
217
+ obs_id,
218
+ max_time,
219
+ min_time,
220
+ max_time,
221
+ ),
222
+ )
223
+ cursor.execute(
224
+ "UPDATE aggregated_events SET start = ?, stop = ? WHERE observation = ? AND start < ? AND stop > ?",
225
+ (
226
+ min_time,
227
+ max_time,
228
+ obs_id,
229
+ min_time,
230
+ max_time,
231
+ ),
232
+ )
233
+
234
+ for ax_idx, subject in enumerate(sorted(distinct_subjects)):
235
+ for behavior in distinct_behav:
236
+ # number of occurences
237
+ cursor.execute(
238
+ ("SELECT COUNT(*) AS count FROM aggregated_events WHERE observation = ? AND subject = ? AND behavior = ?"),
239
+ (
240
+ obs_id,
241
+ subject,
242
+ behavior,
243
+ ),
244
+ )
245
+ for row in cursor.fetchall():
246
+ behaviors[subject][behavior]["number of occurences"] = 0 if row["count"] is None else row["count"]
247
+
248
+ # total duration
249
+ if project_functions.event_type(behavior, pj[cfg.ETHOGRAM]) in cfg.STATE_EVENT_TYPES:
250
+ cursor.execute(
251
+ (
252
+ "SELECT SUM(stop - start) AS duration FROM aggregated_events "
253
+ "WHERE observation = ? AND subject = ? AND behavior = ?"
254
+ ),
255
+ (
256
+ obs_id,
257
+ subject,
258
+ behavior,
259
+ ),
260
+ )
261
+ for row in cursor.fetchall():
262
+ behaviors[subject][behavior]["duration"] = 0 if row["duration"] is None else row["duration"]
263
+
264
+ (
265
+ durations,
266
+ n_occurences,
267
+ colors,
268
+ x_labels,
269
+ colors_duration,
270
+ x_labels_duration,
271
+ ) = ([], [], [], [], [], [])
272
+
273
+ for behavior in sorted(distinct_behav):
274
+ if param[cfg.EXCLUDE_BEHAVIORS] and behaviors[subject][behavior]["number of occurences"] == 0:
275
+ continue
276
+
277
+ n_occurences.append(behaviors[subject][behavior]["number of occurences"])
278
+ x_labels.append(behavior)
279
+
280
+ # color
281
+ behav_idx = [k for k in pj[cfg.ETHOGRAM] if pj[cfg.ETHOGRAM][k]["code"] == behavior][0]
282
+ col = None
283
+ if cfg.COLOR in pj[cfg.ETHOGRAM][behav_idx]:
284
+ col = util.behavior_user_color(pj[cfg.ETHOGRAM], behavior)
285
+ if col is not None:
286
+ colors.append(col)
287
+ else:
288
+ try:
289
+ colors.append(util.behavior_color(plot_colors, all_behaviors.index(behavior)))
290
+ except Exception:
291
+ colors.append("darkgray")
292
+
293
+ if project_functions.event_type(behavior, pj[cfg.ETHOGRAM]) in cfg.STATE_EVENT_TYPES:
294
+ durations.append(behaviors[subject][behavior]["duration"])
295
+ x_labels_duration.append(behavior)
296
+
297
+ col = None
298
+ if cfg.COLOR in pj[cfg.ETHOGRAM][behav_idx]:
299
+ col = util.behavior_user_color(pj[cfg.ETHOGRAM], behavior)
300
+ if col is not None:
301
+ colors_duration.append(col)
302
+ else:
303
+ try:
304
+ colors_duration.append(util.behavior_color(plot_colors, all_behaviors.index(behavior)))
305
+ except Exception:
306
+ colors_duration.append("darkgray")
307
+
308
+ # width = 0.35 # the width of the bars: can also be len(x) sequence
309
+
310
+ axs2[ax_idx].bar(
311
+ np.arange(len(n_occurences)),
312
+ n_occurences,
313
+ # width,
314
+ color=colors,
315
+ )
316
+
317
+ axs[ax_idx].bar(
318
+ np.arange(len(durations)),
319
+ durations,
320
+ # width,
321
+ color=colors_duration,
322
+ )
323
+
324
+ if ax_idx == 0:
325
+ axs[ax_idx].set_ylabel("Duration (s)")
326
+ axs[ax_idx].set_xlabel("Behaviors")
327
+ axs[ax_idx].set_title(f"{subject}")
328
+
329
+ axs[ax_idx].set_xticks(np.arange(len(durations)))
330
+ axs[ax_idx].set_xticklabels(x_labels_duration, rotation="vertical", fontsize=8)
331
+
332
+ if ax_idx == 0:
333
+ axs2[ax_idx].set_ylabel("Number of occurences")
334
+ axs2[ax_idx].set_xlabel("Behaviors")
335
+ axs2[ax_idx].set_title(f"{subject}")
336
+
337
+ axs2[ax_idx].set_xticks(np.arange(len(n_occurences)))
338
+ axs2[ax_idx].set_xticklabels(x_labels, rotation="vertical", fontsize=8)
339
+
340
+ fig.align_labels()
341
+ fig.tight_layout(rect=[0, 0.03, 1, 0.95])
342
+
343
+ fig2.align_labels()
344
+ fig2.tight_layout(rect=[0, 0.03, 1, 0.95])
345
+
346
+ if plot_directory:
347
+ # output_file_name = f"{pathlib.Path(plot_directory) / utilities.safeFileName(obs_id)}.{output_format}"
348
+ fig.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.duration.{output_format}")
349
+ fig2.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.number_of_occurences.{output_format}")
350
+ plt.close()
351
+ else:
352
+ fig.show()
353
+ fig2.show()
354
+
355
+ return {}
356
+
357
+
358
+ def create_events_plot(
359
+ self,
360
+ selected_observations,
361
+ parameters,
362
+ plot_colors=cfg.BEHAVIORS_PLOT_COLORS,
363
+ plot_directory="",
364
+ file_format="png",
365
+ ):
366
+ """
367
+ create a time diagram plot (like a gantt chart)
368
+ with matplotlib barh function (https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.barh.html)
369
+ """
370
+
371
+ selected_subjects = parameters[cfg.SELECTED_SUBJECTS]
372
+ selected_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
373
+ include_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
374
+ interval = parameters[cfg.TIME_INTERVAL]
375
+ start_time = parameters[cfg.START_TIME]
376
+ end_time = parameters[cfg.END_TIME]
377
+
378
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(self.pj, selected_subjects, selected_observations, selected_behaviors)
379
+
380
+ if not ok:
381
+ return False, msg, None
382
+
383
+ cursor = db_connector.cursor()
384
+
385
+ # if modifiers not to be included set modifiers to ""
386
+ if not include_modifiers:
387
+ cursor.execute("UPDATE aggregated_events SET modifiers = ''")
388
+
389
+ cursor.execute("SELECT DISTINCT behavior, modifiers FROM aggregated_events")
390
+ distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
391
+
392
+ # add selected behaviors that are not observed
393
+ for behav in selected_behaviors:
394
+ if [x for x in distinct_behav_modif if x[0] == behav] == []:
395
+ distinct_behav_modif.append([behav, "-"])
396
+
397
+ distinct_behav_modif = sorted(distinct_behav_modif)
398
+ max_len = len(distinct_behav_modif)
399
+
400
+ all_behaviors = [self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(self.pj[cfg.ETHOGRAM])]
401
+
402
+ par1 = 1
403
+ bar_height = 0.5
404
+ epoch_date = dt.datetime(2017, 1, 1)
405
+
406
+ for obs_id in selected_observations:
407
+ if len(selected_subjects) > 1:
408
+ fig, axs = plt.subplots(figsize=(20, 8), nrows=len(selected_subjects), ncols=1, sharex=True)
409
+ else:
410
+ fig, ax = plt.subplots(figsize=(20, 8), nrows=len(selected_subjects), ncols=1, sharex=True)
411
+ axs = np.ndarray(shape=(1), dtype=type(ax))
412
+ axs[0] = ax
413
+
414
+ ok, msg, db_connector = db_functions.load_aggregated_events_in_db(self.pj, selected_subjects, [obs_id], selected_behaviors)
415
+
416
+ cursor = db_connector.cursor()
417
+ # if modifiers not to be included set modifiers to ""
418
+ if not include_modifiers:
419
+ cursor.execute("UPDATE aggregated_events SET modifiers = ''")
420
+ cursor = db_connector.cursor()
421
+
422
+ cursor.execute("SELECT DISTINCT behavior, modifiers FROM aggregated_events")
423
+ distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
424
+
425
+ # add selected behaviors that are not observed
426
+ if not parameters["exclude behaviors"]:
427
+ for behav in selected_behaviors:
428
+ if [x for x in distinct_behav_modif if x[0] == behav] == []:
429
+ distinct_behav_modif.append([behav, "-"])
430
+
431
+ distinct_behav_modif = sorted(distinct_behav_modif)
432
+ max_len = len(distinct_behav_modif)
433
+
434
+ # time
435
+ obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obs_id])
436
+ if obs_length == -1: # media length not available
437
+ interval = cfg.TIME_EVENTS
438
+
439
+ if interval == cfg.TIME_FULL_OBS:
440
+ min_time = 0.0
441
+ max_time = float(obs_length)
442
+
443
+ if interval == cfg.TIME_OBS_INTERVAL:
444
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
445
+ offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
446
+ min_time = float(obs_interval[0]) + offset
447
+ # Use max media duration for max time if no interval is defined (=0)
448
+ max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(obs_length)
449
+
450
+ if interval == cfg.TIME_EVENTS:
451
+ try:
452
+ min_time = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0]) # first event
453
+ except Exception:
454
+ min_time = 0.0
455
+ try:
456
+ max_time = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0]) # last event
457
+ except Exception:
458
+ max_time = float(obs_length)
459
+
460
+ if interval == cfg.TIME_ARBITRARY_INTERVAL:
461
+ min_time = float(start_time)
462
+ max_time = float(end_time)
463
+
464
+ # adjust start if start < init
465
+ cursor.execute(
466
+ "UPDATE aggregated_events SET start = ? WHERE start < ? AND stop BETWEEN ? AND ?",
467
+ (
468
+ min_time,
469
+ min_time,
470
+ min_time,
471
+ max_time,
472
+ ),
473
+ )
474
+ # adjust stop if stop > end
475
+ cursor.execute(
476
+ "UPDATE aggregated_events SET stop = ? WHERE stop > ? AND start BETWEEN ? AND ?",
477
+ (
478
+ max_time,
479
+ max_time,
480
+ min_time,
481
+ max_time,
482
+ ),
483
+ )
484
+ # adjust start and stop if start < init and stop > end
485
+ cursor.execute(
486
+ "UPDATE aggregated_events SET start = ?, stop = ? WHERE start < ? AND stop > ?",
487
+ (
488
+ min_time,
489
+ max_time,
490
+ min_time,
491
+ max_time,
492
+ ),
493
+ )
494
+
495
+ # delete events outside time interval
496
+ cursor.execute(
497
+ "DELETE FROM aggregated_events WHERE (start < ? AND stop < ?) OR (start > ? AND stop > ?)",
498
+ (min_time, min_time, max_time, max_time),
499
+ )
500
+
501
+ ylabels = [" ".join(x) for x in distinct_behav_modif]
502
+ for ax_idx, subject in enumerate(selected_subjects):
503
+ if parameters["exclude behaviors"]:
504
+ cursor.execute(
505
+ "SELECT DISTINCT behavior, modifiers FROM aggregated_events WHERE subject = ?",
506
+ (subject,),
507
+ )
508
+ distinct_behav_modif = [[rows["behavior"], rows["modifiers"]] for rows in cursor.fetchall()]
509
+
510
+ # add selected behaviors that are not observed
511
+ if not parameters["exclude behaviors"]:
512
+ for behav in selected_behaviors:
513
+ if [x for x in distinct_behav_modif if x[0] == behav] == []:
514
+ distinct_behav_modif.append([behav, "-"])
515
+
516
+ distinct_behav_modif = sorted(distinct_behav_modif)
517
+ max_len = len(distinct_behav_modif)
518
+ ylabels = [" ".join(x) for x in distinct_behav_modif]
519
+
520
+ if not ax_idx:
521
+ axs[ax_idx].set_title(f"Observation {obs_id}\n{subject}", fontsize=14)
522
+ else:
523
+ axs[ax_idx].set_title(subject, fontsize=14)
524
+ bars = {}
525
+ i = 0
526
+ for behavior_modifiers in distinct_behav_modif:
527
+ behavior, modifiers = behavior_modifiers
528
+ behavior_modifiers_str = "|".join(behavior_modifiers) if modifiers else behavior
529
+ bars[behavior_modifiers_str] = []
530
+
531
+ # total duration
532
+ cursor.execute(
533
+ ("SELECT start, stop FROM aggregated_events WHERE subject = ? AND behavior = ? AND modifiers = ?"),
534
+ (
535
+ subject,
536
+ behavior,
537
+ modifiers,
538
+ ),
539
+ )
540
+ for row in cursor.fetchall():
541
+ bars[behavior_modifiers_str].append((row["start"], row["stop"]))
542
+
543
+ if self.timeFormat == cfg.HHMMSS:
544
+ start_date = matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=row["start"]))
545
+ end_date = matplotlib.dates.date2num(
546
+ epoch_date + dt.timedelta(seconds=row["stop"] + cfg.POINT_EVENT_PLOT_DURATION * (row["stop"] == row["start"]))
547
+ )
548
+ if self.timeFormat == cfg.S:
549
+ start_date = row["start"]
550
+ end_date = row["stop"]
551
+
552
+ # color
553
+ behav_idx = [k for k in self.pj[cfg.ETHOGRAM] if self.pj[cfg.ETHOGRAM][k]["code"] == behavior][0]
554
+ col = None
555
+ if cfg.COLOR in self.pj[cfg.ETHOGRAM][behav_idx]:
556
+ col = util.behavior_user_color(self.pj[cfg.ETHOGRAM], behavior)
557
+ if col is not None:
558
+ bar_color = col
559
+ else:
560
+ try:
561
+ bar_color = util.behavior_color(plot_colors, all_behaviors.index(behavior))
562
+ except Exception:
563
+ bar_color = "darkgray"
564
+ bar_color = cfg.POINT_EVENT_PLOT_COLOR if row["stop"] == row["start"] else bar_color
565
+
566
+ # sage colors removed from matplotlib colors list
567
+ if bar_color in ("sage", "darksage", "lightsage"):
568
+ bar_color = {
569
+ "darksage": "#598556",
570
+ "lightsage": "#bcecac",
571
+ "sage": "#87ae73",
572
+ }[bar_color]
573
+
574
+ try:
575
+ axs[ax_idx].barh(
576
+ (i * par1) + par1,
577
+ end_date - start_date,
578
+ left=start_date,
579
+ height=bar_height,
580
+ align="center",
581
+ edgecolor=bar_color,
582
+ color=bar_color,
583
+ alpha=1,
584
+ )
585
+ except Exception:
586
+ axs[ax_idx].barh(
587
+ (i * par1) + par1,
588
+ end_date - start_date,
589
+ left=start_date,
590
+ height=bar_height,
591
+ align="center",
592
+ edgecolor="darkgray",
593
+ color="darkgray",
594
+ alpha=1,
595
+ )
596
+
597
+ i += 1
598
+
599
+ axs[ax_idx].set_ylim(bottom=0, top=(max_len * par1) + par1)
600
+ pos = np.arange(par1, max_len * par1 + par1 + 1, par1)
601
+ axs[ax_idx].set_yticks(pos[: len(ylabels)])
602
+
603
+ axs[ax_idx].set_yticklabels(ylabels, fontdict={"fontsize": 10})
604
+
605
+ axs[ax_idx].set_ylabel(
606
+ "Behaviors" + " (modifiers)" * include_modifiers,
607
+ fontdict={"fontsize": 10},
608
+ )
609
+
610
+ if self.timeFormat == cfg.HHMMSS:
611
+ axs[ax_idx].set_xlim(
612
+ left=matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=min_time)),
613
+ right=matplotlib.dates.date2num(epoch_date + dt.timedelta(seconds=max_time)),
614
+ )
615
+
616
+ axs[ax_idx].grid(color="g", linestyle=":")
617
+ if self.timeFormat == cfg.HHMMSS:
618
+ axs[ax_idx].xaxis_date()
619
+ axs[ax_idx].xaxis.set_major_formatter(DateFormatter("%H:%M:%S"))
620
+ axs[ax_idx].set_xlabel("Time (HH:MM:SS)", fontdict={"fontsize": 12})
621
+ if self.timeFormat == cfg.S:
622
+ axs[ax_idx].set_xlabel("Time (s)", fontdict={"fontsize": 12})
623
+
624
+ axs[ax_idx].invert_yaxis()
625
+
626
+ if self.timeFormat == cfg.HHMMSS:
627
+ fig.autofmt_xdate()
628
+
629
+ plt.tight_layout()
630
+
631
+ if len(selected_observations) > 1:
632
+ plt.savefig(f"{pl.Path(plot_directory) / util.safeFileName(obs_id)}.{file_format}")
633
+ else:
634
+ plt.show()