Improve scaling of graph motion activity

This commit is contained in:
Nicolas Mowen 2024-01-29 07:47:53 -07:00
parent ba1b2158dd
commit 48b672e4b4
6 changed files with 117 additions and 92 deletions

View File

@ -784,10 +784,11 @@ def hourly_timeline_activity(camera_name: str):
)
data_type = recording.objects > 0
count = recording.motion + recording.objects
hours[int(key.timestamp())].append(
[
recording.start_time + (recording.duration / 2),
max(recording.motion, recording.objects),
0 if count == 0 else np.log2(count),
data_type,
]
)
@ -801,7 +802,6 @@ def hourly_timeline_activity(camera_name: str):
df.set_index(["date"], inplace=True)
# normalize data
df["count"] = np.clip(np.log10(df["count"], where=df["count"] > 0), None, 10)
df = df.resample("T").mean().fillna(0)
# change types for output

View File

@ -108,6 +108,15 @@ class TimelineProcessor(threading.Thread):
},
}
# update sub labels for existing entries that haven't been added yet
if (
prev_event_data != None
and prev_event_data["sub_label"] != event_data["sub_label"]
and event_id in self.pre_event_cache.keys()
):
for e in self.pre_event_cache[event_id]:
e[Timeline.data]["sub_label"] = event_data["sub_label"]
if event_type == "start":
timeline_entry[Timeline.class_type] = "visible"
save = True

View File

@ -48,7 +48,7 @@ export default function TimelineGraph({
grid: {
show: false,
padding: {
bottom: 20,
bottom: 2,
top: -12,
left: -20,
right: 0,

View File

@ -143,8 +143,8 @@ function ActivityScrubber({
const timelineInstance = new VisTimeline(
divElement,
items as DataItem[],
groups as DataGroup[],
(items || []) as DataItem[],
(groups || []) as DataGroup[],
timelineOptions
);

View File

@ -6,110 +6,124 @@ export function getHourlyTimelineData(
detailLevel: string
): CardsData {
const cards: CardsData = {};
const allHours: { [key: string]: Timeline[] } = {};
timelinePages.forEach((hourlyTimeline) => {
Object.keys(hourlyTimeline["hours"])
.reverse()
.forEach((hour) => {
const day = new Date(parseInt(hour) * 1000);
day.setHours(0, 0, 0, 0);
const dayKey = (day.getTime() / 1000).toString();
Object.entries(hourlyTimeline.hours).forEach(([key, values]) => {
if (key in allHours) {
// only occurs when multiple pages contain elements in the same hour
allHours[key] = allHours[key]
.concat(values)
.sort((a, b) => a.timestamp - b.timestamp);
} else {
allHours[key] = values;
}
});
});
// build a map of course to the types that are included in this hour
// which allows us to know what items to keep depending on detail level
const sourceToTypes: { [key: string]: string[] } = {};
let cardTypeStart: { [camera: string]: number } = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) {
cardTypeStart[i.camera] = i.timestamp;
}
Object.keys(allHours)
.sort((a, b) => a.localeCompare(b))
.reverse()
.forEach((hour) => {
const day = new Date(parseInt(hour) * 1000);
day.setHours(0, 0, 0, 0);
const dayKey = (day.getTime() / 1000).toString();
const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`;
if (groupKey in sourceToTypes) {
sourceToTypes[groupKey].push(i.class_type);
} else {
sourceToTypes[groupKey] = [i.class_type];
}
});
if (!(dayKey in cards)) {
cards[dayKey] = {};
// build a map of course to the types that are included in this hour
// which allows us to know what items to keep depending on detail level
const sourceToTypes: { [key: string]: string[] } = {};
let cardTypeStart: { [camera: string]: number } = {};
Object.values(allHours[hour]).forEach((i) => {
if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) {
cardTypeStart[i.camera] = i.timestamp;
}
if (!(hour in cards[dayKey])) {
cards[dayKey][hour] = {};
const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`;
if (groupKey in sourceToTypes) {
sourceToTypes[groupKey].push(i.class_type);
} else {
sourceToTypes[groupKey] = [i.class_type];
}
});
if (!(dayKey in cards)) {
cards[dayKey] = {};
}
if (!(hour in cards[dayKey])) {
cards[dayKey][hour] = {};
}
let cardStart: { [camera: string]: number } = {};
Object.values(allHours[hour]).forEach((i) => {
if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) {
cardStart[i.camera] = i.timestamp;
}
let cardStart: { [camera: string]: number } = {};
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) {
cardStart[i.camera] = i.timestamp;
}
const time = new Date(i.timestamp * 1000);
const groupKey = `${i.camera}-${cardStart[i.camera]}`;
const sourceKey = `${i.source_id}-${cardStart[i.camera]}`;
const uniqueKey = `${i.source_id}-${i.class_type}`;
const time = new Date(i.timestamp * 1000);
const groupKey = `${i.camera}-${cardStart[i.camera]}`;
const sourceKey = `${i.source_id}-${cardStart[i.camera]}`;
const uniqueKey = `${i.source_id}-${i.class_type}`;
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
// detail level for saving items
// detail level determines which timeline items for each moment is returned
// values can be normal, extra, or full
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
// extra: return all items except attribute / gone / visible unless that is the only item
// full: return all items
let add = true;
const sourceType = sourceToTypes[sourceKey];
let hiddenItems: string[] = [];
if (detailLevel == "normal") {
hiddenItems = [
"active",
"attribute",
"gone",
"stationary",
"visible",
];
} else if (detailLevel == "extra") {
hiddenItems = ["attribute", "gone", "visible"];
}
let add = true;
const sourceType = sourceToTypes[sourceKey];
let hiddenItems: string[] = [];
if (detailLevel == "normal") {
hiddenItems = [
"active",
"attribute",
"gone",
"stationary",
"visible",
];
} else if (detailLevel == "extra") {
hiddenItems = ["attribute", "gone", "visible"];
}
if (sourceType.length > 1) {
// we have multiple timeline items for this card
if (sourceType.length > 1) {
// we have multiple timeline items for this card
if (
sourceType.find((type) => hiddenItems.includes(type) == false) ==
undefined
) {
// all of the attribute items for this card make it hidden, but we need to show one
if (sourceType.indexOf(i.class_type) != 0) {
add = false;
}
} else if (hiddenItems.includes(i.class_type)) {
if (
sourceType.find((type) => hiddenItems.includes(type) == false) ==
undefined
) {
// all of the attribute items for this card make it hidden, but we need to show one
if (sourceType.indexOf(i.class_type) != 0) {
add = false;
}
} else if (hiddenItems.includes(i.class_type)) {
add = false;
}
}
if (add) {
if (groupKey in cards[dayKey][hour]) {
if (
!cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) ||
detailLevel == "full"
) {
cards[dayKey][hour][groupKey].entries.push(i);
cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey);
}
} else {
cards[dayKey][hour][groupKey] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
uniqueKeys: [uniqueKey],
};
if (add) {
if (groupKey in cards[dayKey][hour]) {
if (
!cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) ||
detailLevel == "full"
) {
cards[dayKey][hour][groupKey].entries.push(i);
cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey);
}
} else {
cards[dayKey][hour][groupKey] = {
camera: i.camera,
time: time.getTime() / 1000,
entries: [i],
uniqueKeys: [uniqueKey],
};
}
});
}
});
});
});
return cards;
}

View File

@ -162,7 +162,6 @@ export default function DesktopTimelineView({
{isSelected ? (
<div className="p-2 relative bg-secondary bg-opacity-30 rounded-md">
<ActivityScrubber
items={[]}
timeBars={
isSelected
? [
@ -180,7 +179,10 @@ export default function DesktopTimelineView({
snap: null,
min: new Date(timeline.range.start * 1000),
max: new Date(timeline.range.end * 1000),
start: new Date(timeline.range.start * 1000),
end: new Date(timeline.range.end * 1000),
zoomable: false,
height: "120px",
}}
timechangeHandler={(data) => {
controllerRef.current?.scrubToTimestamp(