Skip to content
Merged
2 changes: 2 additions & 0 deletions ami/main/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1217,8 +1217,10 @@ class EventTimelineIntervalSerializer(serializers.Serializer):
start = serializers.DateTimeField()
end = serializers.DateTimeField()
first_capture = EventTimelineSourceImageSerializer(allow_null=True)
top_capture = EventTimelineSourceImageSerializer(allow_null=True)
captures_count = serializers.IntegerField()
detections_count = serializers.IntegerField()
detections_avg = serializers.IntegerField()


class EventTimelineMetaSerializer(serializers.Serializer):
Expand Down
11 changes: 11 additions & 0 deletions ami/main/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import logging
from statistics import mode

from django.contrib.postgres.search import TrigramSimilarity
from django.core import exceptions
Expand Down Expand Up @@ -280,8 +281,10 @@ def timeline(self, request, pk=None):
"start": current_time,
"end": interval_end,
"first_capture": None,
"top_capture": None,
"captures_count": 0,
"detections_count": 0,
"detection_counts": [],
}

while image_index < len(source_images) and source_images[image_index]["timestamp"] <= interval_end:
Expand All @@ -290,8 +293,16 @@ def timeline(self, request, pk=None):
interval_data["first_capture"] = SourceImage(pk=image["id"])
interval_data["captures_count"] += 1
interval_data["detections_count"] += image["detections_count"] or 0
interval_data["detection_counts"] += [image["detections_count"]]
if image["detections_count"] >= max(interval_data["detection_counts"]):
interval_data["top_capture"] = SourceImage(pk=image["id"])
image_index += 1

# Set a meaningful average detection count to display for the interval
# Remove zero values and calculate the mode
interval_data["detection_counts"] = [x for x in interval_data["detection_counts"] if x > 0]
interval_data["detections_avg"] = mode(interval_data["detection_counts"] or [0])

timeline.append(interval_data)
current_time = interval_end

Expand Down
22 changes: 21 additions & 1 deletion ui/src/data-services/models/timeline-tick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ export type ServerTimelineTick = {
first_capture: {
id: number
} | null
top_capture: {
id: number
} | null
captures_count: number
detections_count: number
detections_avg: number
}

export class TimelineTick {
Expand All @@ -25,6 +29,10 @@ export class TimelineTick {
return this._timelineTick.detections_count ?? 0
}

get avgDetections(): number {
return this._timelineTick.detections_avg ?? 0
}

get numCaptures(): number {
return this._timelineTick.captures_count ?? 0
}
Expand All @@ -41,6 +49,18 @@ export class TimelineTick {
return `${this._timelineTick.first_capture.id}`
}

get topCaptureId(): string | undefined {
if (!this._timelineTick.top_capture) {
return undefined
}

return `${this._timelineTick.top_capture.id}`
}

get representativeCaptureId(): string | undefined {
return this.topCaptureId ?? this.firstCaptureId
}

get tooltip(): string {
const timespanString = getCompactTimespanString({
date1: this.startDate,
Expand All @@ -50,6 +70,6 @@ export class TimelineTick {
},
})

return `${timespanString}<br>Captures: ${this.numCaptures}<br>Detections: ${this.numDetections}`
return `${timespanString}<br>Captures: ${this.numCaptures}<br>Avg. Detections: ${this.avgDetections}`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const lineColorDetections = '#5f8ac6'
const textColor = '#222426'
const tooltipBgColor = '#ffffff'
const tooltipBorderColor = '#222426'
const gridLineColor = '#f36399'

export const ActivityPlot = ({
session,
Expand All @@ -22,14 +23,30 @@ export const ActivityPlot = ({
}) => {
const containerRef = useRef(null)
const width = useDynamicPlotWidth(containerRef)

// Calculate the average number of captures
const avgCaptures =
timeline.reduce((sum, tick) => sum + tick.numCaptures, 0) / timeline.length

// Calculate the maximum deviation from the average
const maxDeviation = Math.max(
...timeline.map((tick) => Math.abs(tick.numCaptures - avgCaptures))
)

// Set the y-axis range to be centered around the average
const yAxisMin = Math.max(0, avgCaptures - maxDeviation)
const yAxisMax = avgCaptures + maxDeviation

return (
<div style={{ margin: '0 14px -10px' }}>
<div ref={containerRef}>
<Plot
style={{ display: 'block' }}
data={[
{
x: timeline.map((timelineTick) => timelineTick.startDate),
x: timeline.map(
(timelineTick) => new Date(timelineTick.startDate)
),
y: timeline.map((timelineTick) => timelineTick.numCaptures),
text: timeline.map((timelineTick) => timelineTick.tooltip),
hovertemplate: '%{text}<extra></extra>',
Expand All @@ -38,17 +55,21 @@ export const ActivityPlot = ({
mode: 'lines',
line: { color: lineColorCaptures, width: 1 },
name: 'Captures',
yaxis: 'y',
},
{
x: timeline.map((timelineTick) => timelineTick.startDate),
y: timeline.map((timelineTick) => timelineTick.numDetections),
x: timeline.map(
(timelineTick) => new Date(timelineTick.startDate)
),
y: timeline.map((timelineTick) => timelineTick.avgDetections),
text: timeline.map((timelineTick) => timelineTick.tooltip),
hovertemplate: '%{text}<extra></extra>',
fill: 'tozeroy',
type: 'scatter',
mode: 'lines',
line: { color: lineColorDetections, width: 1 },
name: 'Detections',
name: 'Avg. Detections',
yaxis: 'y2',
},
]}
layout={{
Expand All @@ -69,14 +90,30 @@ export const ActivityPlot = ({
zeroline: false,
rangemode: 'nonnegative',
fixedrange: true,
range: [yAxisMin, yAxisMax],
side: 'left',
},
xaxis: {
showline: true,
yaxis2: {
showgrid: false,
showticklabels: false,
zeroline: false,
rangemode: 'nonnegative',
fixedrange: true,
range: [0, session.detectionsMaxCount],
side: 'right',
overlaying: 'y',
},
xaxis: {
showline: false,
showgrid: true,
griddash: 'dot',
gridwidth: 1,
gridcolor: gridLineColor,
showticklabels: false,
zeroline: false,
fixedrange: true,
range: [session.startDate, session.endDate],
range: [new Date(session.startDate), new Date(session.endDate)],
Copy link
Member

@annavik annavik Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session.startDate is already a date, any reason we are creating a new date? Same in some other cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I was experimenting with date formats was trying different approaches. Reverting!

dtick: 3600000, // milliseconds in an hour
},
hoverlabel: {
bgcolor: tooltipBgColor,
Expand All @@ -95,8 +132,8 @@ export const ActivityPlot = ({
onClick={(data) => {
const timelineTickIndex = data.points[0].pointIndex
const timelineTick = timeline[timelineTickIndex]
if (timelineTick?.firstCaptureId) {
setActiveCaptureId(timelineTick.firstCaptureId)
if (timelineTick?.representativeCaptureId) {
setActiveCaptureId(timelineTick.representativeCaptureId)
}
}}
/>
Expand Down