2828
2929
3030def captures_per_hour (project_pk : int ):
31- # Captures per hour
31+ # Average captures per hour across all days
3232 SourceImage = apps .get_model ("main" , "SourceImage" )
33- captures_per_hour = list (
33+
34+ # First get captures per hour per day
35+ captures_by_day_hour = (
3436 SourceImage .objects .filter (project = project_pk )
35- .values ("timestamp__hour" )
36- .annotate (num_captures = models .Count ("pk" ))
37- .order_by ("timestamp__hour" )
3837 .exclude (timestamp = None )
38+ .values ("timestamp__date" , "timestamp__hour" )
39+ .annotate (count = models .Count ("pk" ))
40+ .order_by ("timestamp__date" , "timestamp__hour" )
3941 )
4042
41- if captures_per_hour :
42- hours , counts = list (zip (* captures_per_hour ))
43- hours , counts = list (zip (* [(d ["timestamp__hour" ], d ["num_captures" ]) for d in captures_per_hour ]))
44- # hours = map(int, hours)
45- hours , counts = shift_to_nighttime (list (hours ), list (counts ))
46- # @TODO show a tick for every hour even if there are no captures
47- hours = [datetime .datetime .strptime (str (h ), "%H" ).strftime ("%-I:00 %p" ) for h in hours ]
48- ticktext = [f"{ hours [0 ]} :00" , f"{ hours [- 1 ]} :00" ]
43+ # Calculate average per hour
44+ hour_totals = {}
45+ hour_counts = {}
46+
47+ for entry in captures_by_day_hour :
48+ hour = entry ["timestamp__hour" ]
49+ count = entry ["count" ]
50+
51+ if hour not in hour_totals :
52+ hour_totals [hour ] = 0
53+ hour_counts [hour ] = 0
54+
55+ hour_totals [hour ] += count
56+ hour_counts [hour ] += 1
57+
58+ # Calculate averages
59+ avg_captures_per_hour = [
60+ {"hour" : hour , "avg_captures" : round (hour_totals [hour ] / hour_counts [hour ], 0 )} for hour in hour_totals .keys ()
61+ ]
62+ avg_captures_per_hour .sort (key = lambda x : x ["hour" ])
63+
64+ if avg_captures_per_hour :
65+ hours = [entry ["hour" ] for entry in avg_captures_per_hour ]
66+ avgs = [entry ["avg_captures" ] for entry in avg_captures_per_hour ]
4967
68+ hours , avgs = shift_to_nighttime (hours , avgs )
69+ hours = [datetime .datetime .strptime (str (h ), "%H" ).strftime ("%-I:00 %p" ) for h in hours ]
70+ ticktext = [f"{ hours [0 ]} " , f"{ hours [- 1 ]} " ]
5071 else :
51- hours , counts = [], []
72+ hours , avgs = [], []
5273 ticktext = []
5374
5475 return {
55- "title" : "Captures per hour" ,
56- "data" : {"x" : hours , "y" : counts , "ticktext" : ticktext },
76+ "title" : "Average captures per hour" ,
77+ "data" : {"x" : hours , "y" : avgs , "ticktext" : ticktext },
5778 "type" : "bar" ,
5879 }
5980
@@ -168,32 +189,54 @@ def events_per_month(project_pk: int):
168189
169190
170191def detections_per_hour (project_pk : int ):
171- # Detections per hour
192+ # Average detections per hour across all days
172193 Detection = apps .get_model ("main" , "Detection" )
173- detections_per_hour = list (
194+
195+ # First get detections per hour per day
196+ detections_by_day_hour = (
174197 Detection .objects .filter (occurrence__project = project_pk )
175- .values ("source_image__timestamp__hour" )
176- .annotate (num_detections = models .Count ("id" ))
177- .order_by ("source_image__timestamp__hour" )
178198 .exclude (source_image__timestamp = None )
199+ .values ("source_image__timestamp__date" , "source_image__timestamp__hour" )
200+ .annotate (count = models .Count ("id" ))
201+ .order_by ("source_image__timestamp__date" , "source_image__timestamp__hour" )
179202 )
180203
181- # hours, counts = list(zip(*detections_per_hour))
182- if detections_per_hour :
183- hours , counts = list (
184- zip (* [(d ["source_image__timestamp__hour" ], d ["num_detections" ]) for d in detections_per_hour ])
185- )
186- hours , counts = shift_to_nighttime (list (hours ), list (counts ))
187- # @TODO show a tick for every hour even if there are no detections
204+ # Calculate average per hour
205+ hour_totals = {}
206+ hour_counts = {}
207+
208+ for entry in detections_by_day_hour :
209+ hour = entry ["source_image__timestamp__hour" ]
210+ count = entry ["count" ]
211+
212+ if hour not in hour_totals :
213+ hour_totals [hour ] = 0
214+ hour_counts [hour ] = 0
215+
216+ hour_totals [hour ] += count
217+ hour_counts [hour ] += 1
218+
219+ # Calculate averages
220+ avg_detections_per_hour = [
221+ {"hour" : hour , "avg_detections" : round (hour_totals [hour ] / hour_counts [hour ], 0 )}
222+ for hour in hour_totals .keys ()
223+ ]
224+ avg_detections_per_hour .sort (key = lambda x : x ["hour" ])
225+
226+ if avg_detections_per_hour :
227+ hours = [entry ["hour" ] for entry in avg_detections_per_hour ]
228+ avgs = [entry ["avg_detections" ] for entry in avg_detections_per_hour ]
229+
230+ hours , avgs = shift_to_nighttime (hours , avgs )
188231 hours = [datetime .datetime .strptime (str (h ), "%H" ).strftime ("%-I:00 %p" ) for h in hours ]
189- ticktext = [f"{ hours [0 ]} :00 " , f"{ hours [- 1 ]} :00 " ]
232+ ticktext = [f"{ hours [0 ]} " , f"{ hours [- 1 ]} " ]
190233 else :
191- hours , counts = [], []
234+ hours , avgs = [], []
192235 ticktext = []
193236
194237 return {
195- "title" : "Detections per hour" ,
196- "data" : {"x" : hours , "y" : counts , "ticktext" : ticktext },
238+ "title" : "Average detections per hour" ,
239+ "data" : {"x" : hours , "y" : avgs , "ticktext" : ticktext },
197240 "type" : "bar" ,
198241 }
199242
@@ -263,7 +306,7 @@ def event_detections_per_hour(event_pk: int):
263306
264307
265308def event_top_taxa (event_pk : int , top_n : int = 10 ):
266- # Horiziontal bar chart of top taxa
309+ # Horizontal bar chart of top taxa
267310 Taxon = apps .get_model ("main" , "Taxon" )
268311 top_taxa = (
269312 Taxon .objects .filter (occurrences__event = event_pk )
@@ -274,21 +317,96 @@ def event_top_taxa(event_pk: int, top_n: int = 10):
274317 )
275318
276319 if top_taxa :
277- taxa , counts = list (zip (* [(t ["name" ], t ["num_detections" ]) for t in top_taxa ]))
320+ taxa , counts = list (zip (* [(t ["name" ], t ["num_detections" ]) for t in reversed ( top_taxa ) ]))
278321 taxa = [t or "Unknown" for t in taxa ]
279322 counts = [c or 0 for c in counts ]
280323 else :
281324 taxa , counts = [], []
282325
283- # Restrict number of top species if too many
284- MAX_SPECIES = 10
285- if len (taxa ) > MAX_SPECIES :
286- taxa = taxa [:MAX_SPECIES ]
287- counts = counts [:MAX_SPECIES ]
288-
289326 return {
290327 "title" : "Top species" ,
291328 "data" : {"x" : counts , "y" : taxa },
292329 "type" : "bar" ,
293330 "orientation" : "h" ,
294331 }
332+
333+
334+ def project_top_taxa (project_pk : int , top_n : int = 10 ):
335+ Taxon = apps .get_model ("main" , "Taxon" )
336+ top_taxa = (
337+ Taxon .objects .all ()
338+ .with_occurrence_counts (project = project_pk ) # type: ignore
339+ .order_by ("-occurrence_count" )[:top_n ]
340+ )
341+
342+ if top_taxa :
343+ taxa , counts = list (zip (* [(t .name , t .occurrence_count ) for t in reversed (top_taxa )]))
344+ else :
345+ taxa , counts = [], []
346+
347+ return {
348+ "title" : "Top species observed" ,
349+ "data" : {"x" : counts , "y" : taxa },
350+ "type" : "bar" ,
351+ "orientation" : "h" ,
352+ }
353+
354+
355+ def unique_species_per_month (project_pk : int ):
356+ # Unique species per month
357+ Occurrence = apps .get_model ("main" , "Occurrence" )
358+ unique_species_per_month = (
359+ Occurrence .objects .filter (project = project_pk )
360+ .values_list ("event__start__month" )
361+ .annotate (num_species = models .Count ("determination_id" , distinct = True ))
362+ .order_by ("event__start__month" )
363+ )
364+
365+ # Create a dictionary mapping month numbers to species counts
366+ month_to_count = {month : count for month , count in unique_species_per_month }
367+
368+ # Create lists for all 12 months, using 0 for months with no data
369+ all_months = list (range (1 , 13 )) # 1-12 for January-December
370+ counts = [month_to_count .get (month , 0 ) for month in all_months ]
371+
372+ # Generate labels for all months
373+ labels = [datetime .date (3000 , month , 1 ).strftime ("%b" ) for month in all_months ]
374+
375+ # Show all months as tick values
376+ tickvals = ["Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec" ]
377+
378+ return {
379+ "title" : "Unique species per month" ,
380+ "data" : {"x" : labels , "y" : counts , "tickvals" : tickvals },
381+ "type" : "bar" ,
382+ }
383+
384+
385+ def average_occurrences_per_month (project_pk : int ):
386+ # Average occurrences per month
387+ Occurrence = apps .get_model ("main" , "Occurrence" )
388+ occurrences_per_month = (
389+ Occurrence .objects .filter (project = project_pk )
390+ .values_list ("event__start__month" )
391+ .annotate (num_occurrences = models .Count ("id" ))
392+ .order_by ("event__start__month" )
393+ )
394+
395+ # Create a dictionary mapping month numbers to occurrence counts
396+ month_to_count = {month : count for month , count in occurrences_per_month }
397+
398+ # Create lists for all 12 months, using 0 for months with no data
399+ all_months = list (range (1 , 13 )) # 1-12 for January-December
400+ counts = [month_to_count .get (month , 0 ) for month in all_months ]
401+
402+ # Generate labels for all months
403+ labels = [datetime .date (3000 , month , 1 ).strftime ("%b" ) for month in all_months ]
404+
405+ # Show all months as tick vals
406+ tickvals = ["Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec" ]
407+
408+ return {
409+ "title" : "Average occurrences per month" ,
410+ "data" : {"x" : labels , "y" : counts , "tickvals" : tickvals },
411+ "type" : "bar" ,
412+ }
0 commit comments