Add graph to timeline

This commit is contained in:
Nick Mowen 2024-01-02 15:43:23 -07:00
parent e110a7d11d
commit 0f38eb69bd
7 changed files with 139 additions and 33 deletions

View File

@ -751,28 +751,6 @@ def hourly_timeline_activity(camera_name: str):
} }
) )
# process data to make data counts relative
for hour, data in hours.items():
motion_values = np.asarray(list(map(lambda m: m["count"], data)))
avg = motion_values.mean()
std = motion_values.std()
for idx, motion in enumerate(motion_values):
if motion < (avg - (std * 2)):
value = 1
elif motion < (avg - std):
value = 2
elif motion < avg:
value = 3
elif motion < (avg + std):
value = 4
elif motion < (avg + (std * 2)):
value = 5
else:
value = 6
hours[hour][idx]["count"] = value
return jsonify(hours) return jsonify(hours)

View File

@ -54,6 +54,8 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) {
labels: { labels: {
show: false, show: false,
}, },
logarithmic: true,
logBase: 10,
}, },
}} }}
series={data} series={data}

View File

@ -9,6 +9,8 @@ import { Event } from "@/types/event";
import ActivityIndicator from "@/components/ui/activity-indicator"; import ActivityIndicator from "@/components/ui/activity-indicator";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import TimelineScrubber from "@/components/playground/TimelineScrubber"; import TimelineScrubber from "@/components/playground/TimelineScrubber";
import TimelineGraph from "@/components/graph/TimelineGraph";
import { GraphDataPoint } from "@/types/graph";
// Color data // Color data
const colors = [ const colors = [
@ -74,6 +76,36 @@ function UIPlayground() {
"events", "events",
{ limit: 10, after: recentTimestamp }, { limit: 10, after: recentTimestamp },
]); ]);
const { data: recordingSegments } = useSWR<RecordingSegment[]>([
"front_cam/recordings",
{ before: 1704211200, after: 1704207600 },
]);
const graphData = useMemo(() => {
if (!recordingSegments) {
return { motion: [], objects: [] };
}
const motion: GraphDataPoint[] = [];
const objects: GraphDataPoint[] = [];
recordingSegments
.forEach((seg) => {
if (seg.objects > 0) {
objects.push({
x: new Date((seg.start_time + 5) * 1000),
y: seg.motion,
});
} else {
motion.push({
x: new Date((seg.start_time + 5) * 1000),
y: seg.motion,
});
}
});
return { motion, objects };
}, [recordingSegments]);
return ( return (
<> <>
@ -91,12 +123,28 @@ function UIPlayground() {
{config && ( {config && (
<div> <div>
{events && events.length > 0 && ( {events && events.length > 0 && (
<> <div className="relative">
<ActivityScrubber <ActivityScrubber
items={eventsToScrubberItems(events)} items={[]}
options={{
start: new Date(1704207600000),
end: new Date(1704211200000),
}}
selectHandler={onSelect} selectHandler={onSelect}
/> />
</> <div className="w-full absolute left-0 top-0 h-[84px]">
<TimelineGraph
id="test"
data={[
{
name: "Motion",
data: graphData.motion,
},
{ name: "Active Objects", data: graphData.objects },
]}
/>
</div>
</div>
)} )}
</div> </div>
)} )}

View File

@ -56,6 +56,12 @@ interface HistoryFilter extends FilterType {
detailLevel: "normal" | "extra" | "full"; detailLevel: "normal" | "extra" | "full";
} }
type HistoryTimeline = {
start: number;
end: number;
playbackItems: TimelinePlayback[];
};
type TimelinePlayback = { type TimelinePlayback = {
camera: string; camera: string;
range: { start: number; end: number }; range: { start: number; end: number };

View File

@ -18,3 +18,13 @@ type RecordingSegment = {
objects: number; objects: number;
segment_size: number; segment_size: number;
}; };
type RecordingActivity = {
[hour: number]: RecordingSegmentActivity[];
};
type RecordingSegmentActivity = {
date: number;
count: number;
type: "motion" | "objects";
};

View File

@ -107,13 +107,14 @@ export function getTimelineHoursForDay(
cards: CardsData, cards: CardsData,
allPreviews: Preview[], allPreviews: Preview[],
timestamp: number timestamp: number
): TimelinePlayback[] { ): HistoryTimeline {
const now = new Date(); const now = new Date();
const data: TimelinePlayback[] = []; const data: TimelinePlayback[] = [];
const startDay = new Date(timestamp * 1000); const startDay = new Date(timestamp * 1000);
startDay.setHours(23, 59, 59, 999); startDay.setHours(23, 59, 59, 999);
const dayEnd = startDay.getTime() / 1000; const dayEnd = startDay.getTime() / 1000;
startDay.setHours(0, 0, 0, 0); startDay.setHours(0, 0, 0, 0);
const startTimestamp = startDay.getTime() / 1000;
let start = startDay.getTime() / 1000; let start = startDay.getTime() / 1000;
let end = 0; let end = 0;
@ -134,7 +135,7 @@ export function getTimelineHoursForDay(
}); });
if (dayIdx == undefined) { if (dayIdx == undefined) {
return []; return { start: 0, end: 0, playbackItems: [] };
} }
const day = cards[dayIdx]; const day = cards[dayIdx];
@ -179,5 +180,5 @@ export function getTimelineHoursForDay(
start = startDay.getTime() / 1000; start = startDay.getTime() / 1000;
} }
return data.reverse(); return { start: startTimestamp, end, playbackItems: data.reverse() };
} }

View File

@ -10,6 +10,8 @@ import useSWR from "swr";
import Player from "video.js/dist/types/player"; import Player from "video.js/dist/types/player";
import TimelineItemCard from "@/components/card/TimelineItemCard"; import TimelineItemCard from "@/components/card/TimelineItemCard";
import { getTimelineHoursForDay } from "@/utils/historyUtil"; import { getTimelineHoursForDay } from "@/utils/historyUtil";
import { GraphDataPoint } from "@/types/graph";
import TimelineGraph from "@/components/graph/TimelineGraph";
type DesktopTimelineViewProps = { type DesktopTimelineViewProps = {
timelineData: CardsData; timelineData: CardsData;
@ -166,6 +168,50 @@ export default function DesktopTimelineView({
[] []
); );
const { data: activity } = useSWR<RecordingActivity>(
[
`${initialPlayback.camera}/recording/hourly/activity`,
{
after: timelineStack.start,
before: timelineStack.end,
timezone,
},
],
{ revalidateOnFocus: false }
);
const timelineGraphData = useMemo(() => {
if (!activity) {
return {};
}
const graphData: {
[hour: string]: { objects: GraphDataPoint[]; motion: GraphDataPoint[] };
} = {};
Object.entries(activity).forEach(([hour, data]) => {
const objects: GraphDataPoint[] = [];
const motion: GraphDataPoint[] = [];
data.forEach((seg) => {
if (seg.type == "objects") {
objects.push({
x: new Date(seg.date * 1000),
y: seg.count,
});
} else {
motion.push({
x: new Date(seg.date * 1000),
y: seg.count,
});
}
});
graphData[hour] = { objects, motion };
});
return graphData;
}, [activity]);
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -271,14 +317,18 @@ export default function DesktopTimelineView({
</div> </div>
</div> </div>
<div className="m-1 max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto"> <div className="m-1 max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto">
{timelineStack.map((timeline) => { {timelineStack.playbackItems.map((timeline) => {
const isSelected = const isSelected =
timeline.range.start == selectedPlayback.range.start; timeline.range.start == selectedPlayback.range.start;
const graphData = timelineGraphData[timeline.range.start];
return ( return (
<div <div
key={timeline.range.start} key={timeline.range.start}
className={`p-2 ${isSelected ? "bg-secondary bg-opacity-30 rounded-md" : ""}`} className={`relative p-2 ${
isSelected ? "bg-secondary bg-opacity-30 rounded-md" : ""
}`}
onClick={() => setSelectedPlayback(timeline)}
> >
<ActivityScrubber <ActivityScrubber
items={[]} items={[]}
@ -324,9 +374,6 @@ export default function DesktopTimelineView({
setScrubbing(false); setScrubbing(false);
playerRef.current?.play(); playerRef.current?.play();
}} }}
doubleClickHandler={() => {
setSelectedPlayback(timeline);
}}
selectHandler={(data) => { selectHandler={(data) => {
if (data.items.length > 0) { if (data.items.length > 0) {
const selected = data.items[0]; const selected = data.items[0];
@ -338,6 +385,20 @@ export default function DesktopTimelineView({
} }
}} }}
/> />
{graphData && (
<div className="w-full absolute left-0 top-0 h-[84px]">
<TimelineGraph
id={timeline.range.start.toString()}
data={[
{
name: "Motion",
data: graphData.motion,
},
{ name: "Active Objects", data: graphData.objects },
]}
/>
</div>
)}
</div> </div>
); );
})} })}