From ee3bc1be7df5ba9bf41918410edfc7848be45ed9 Mon Sep 17 00:00:00 2001 From: Vanessa Mac Date: Mon, 19 Jan 2026 00:56:14 -0500 Subject: [PATCH 1/3] Add null detections test; update save_results --- ami/ml/models/pipeline.py | 42 +++++++++++++++++++++++++++++++++++++-- ami/ml/schemas.py | 4 ++-- ami/ml/tests.py | 19 ++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/ami/ml/models/pipeline.py b/ami/ml/models/pipeline.py index f76822e3a..413178389 100644 --- a/ami/ml/models/pipeline.py +++ b/ami/ml/models/pipeline.py @@ -8,6 +8,7 @@ import collections import dataclasses +import datetime import logging import time import typing @@ -37,7 +38,7 @@ update_occurrence_determination, ) from ami.ml.exceptions import PipelineNotConfigured -from ami.ml.models.algorithm import Algorithm, AlgorithmCategoryMap +from ami.ml.models.algorithm import Algorithm, AlgorithmCategoryMap, AlgorithmTaskType from ami.ml.schemas import ( AlgorithmConfigResponse, AlgorithmReference, @@ -406,7 +407,10 @@ def get_or_create_detection( :return: A tuple of the Detection object and a boolean indicating whether it was created """ - serialized_bbox = list(detection_resp.bbox.dict().values()) + if detection_resp.bbox is not None: + serialized_bbox = list(detection_resp.bbox.dict().values()) + else: + serialized_bbox = None detection_repr = f"Detection {detection_resp.source_image_id} {serialized_bbox}" assert str(detection_resp.source_image_id) == str( @@ -485,6 +489,7 @@ def create_detections( existing_detections: list[Detection] = [] new_detections: list[Detection] = [] + for detection_resp in detections: source_image = source_image_map.get(detection_resp.source_image_id) if not source_image: @@ -866,6 +871,39 @@ def save_results( "Algorithms and category maps must be registered before processing, using /info endpoint." ) + # Ensure all images have detections + # if not, add a NULL detection (empty bbox) to the results + source_images_with_detections = [detection.source_image_id for detection in results.detections] + source_images_with_detections = set(source_images_with_detections) + null_detections_to_add = [] + + for source_img in results.source_images: + if source_img.id not in source_images_with_detections: + detector_algorithm_reference = None + for known_algorithm in algorithms_known.values(): + if known_algorithm.task_type == AlgorithmTaskType.DETECTION: + detector_algorithm_reference = AlgorithmReference( + name=known_algorithm.name, key=known_algorithm.key + ) + + if detector_algorithm_reference is None: + job_logger.error( + f"Could not identify the detector algorithm. " + f"A null detection was not created for Source Image {source_img.id}" + ) + continue + + null_detections_to_add.append( + DetectionResponse( + source_image_id=source_img.id, + bbox=None, + algorithm=detector_algorithm_reference, + timestamp=datetime.datetime.now(), + ) + ) + + results.detections = results.detections + null_detections_to_add + detections = create_detections( detections=results.detections, algorithms_known=algorithms_known, diff --git a/ami/ml/schemas.py b/ami/ml/schemas.py index 478b4c8fd..b18105163 100644 --- a/ami/ml/schemas.py +++ b/ami/ml/schemas.py @@ -136,14 +136,14 @@ class Config: class DetectionRequest(pydantic.BaseModel): source_image: SourceImageRequest # the 'original' image - bbox: BoundingBox + bbox: BoundingBox | None = None crop_image_url: str | None = None algorithm: AlgorithmReference class DetectionResponse(pydantic.BaseModel): source_image_id: str - bbox: BoundingBox + bbox: BoundingBox | None = None inference_time: float | None = None algorithm: AlgorithmReference timestamp: datetime.datetime diff --git a/ami/ml/tests.py b/ami/ml/tests.py index 14e4374f2..4ef3ca9e4 100644 --- a/ami/ml/tests.py +++ b/ami/ml/tests.py @@ -687,6 +687,25 @@ def test_project_pipeline_config(self): final_config = self.pipeline.get_config(self.project.pk) self.assertEqual(final_config["test_param"], "project_value") + def test_image_with_null_detection(self): + """ + Test saving results for a pipeline that returns null detections for some images. + """ + results = self.fake_pipeline_results(self.test_images, self.pipeline) + + # Manually change the results for a single image to a list of empty detections + first_image_id = results.source_images[0].id + new_detections = [detection for detection in results.detections if detection.source_image_id == first_image_id] + results.detections = new_detections + + save_results(results) + + # After save is done, each image should have at least one detection, + # even if the detection list from the PS was empty. + for image in self.test_images: + image.save() + self.assertEqual(image.detections_count, 1) + class TestAlgorithmCategoryMaps(TestCase): def setUp(self): From 573dee5ada228033b375ed55dfac2b8aedc6d638 Mon Sep 17 00:00:00 2001 From: Vanessa Mac Date: Wed, 11 Feb 2026 11:25:15 -0500 Subject: [PATCH 2/3] Unit test, display processed timeline and collection stats --- ami/main/admin.py | 1 + ami/main/api/serializers.py | 2 + ami/main/api/views.py | 12 +++-- ami/main/models.py | 44 +++++++++++++++++-- ami/ml/tests.py | 23 ++++++---- ui/src/data-services/models/collection.ts | 14 ++++++ ui/src/data-services/models/timeline-tick.ts | 5 +++ .../collections/collection-columns.tsx | 10 +++++ .../pages/project/collections/collections.tsx | 1 + .../playback/activity-plot/activity-plot.tsx | 27 ++++++++++++ ui/src/utils/language.ts | 2 + 11 files changed, 126 insertions(+), 15 deletions(-) diff --git a/ami/main/admin.py b/ami/main/admin.py index 215d8a3be..49e7ebb5c 100644 --- a/ami/main/admin.py +++ b/ami/main/admin.py @@ -266,6 +266,7 @@ class SourceImageAdmin(AdminBase): "checksum", "checksum_algorithm", "created_at", + "get_was_processed", ) list_filter = ( diff --git a/ami/main/api/serializers.py b/ami/main/api/serializers.py index 1c5b7a126..9957fedb3 100644 --- a/ami/main/api/serializers.py +++ b/ami/main/api/serializers.py @@ -1178,6 +1178,7 @@ class Meta: "source_images", "source_images_count", "source_images_with_detections_count", + "source_images_processed_count", "occurrences_count", "taxa_count", "description", @@ -1479,6 +1480,7 @@ class EventTimelineIntervalSerializer(serializers.Serializer): captures_count = serializers.IntegerField() detections_count = serializers.IntegerField() detections_avg = serializers.IntegerField() + was_processed = serializers.BooleanField() class EventTimelineMetaSerializer(serializers.Serializer): diff --git a/ami/main/api/views.py b/ami/main/api/views.py index 9a2770ac8..2e86a66b0 100644 --- a/ami/main/api/views.py +++ b/ami/main/api/views.py @@ -375,7 +375,7 @@ def timeline(self, request, pk=None): ) resolution = datetime.timedelta(minutes=resolution_minutes) - qs = SourceImage.objects.filter(event=event) + qs = SourceImage.objects.filter(event=event).with_was_processed() # type: ignore # Bulk update all source images where detections_count is null update_detection_counts(qs=qs, null_only=True) @@ -401,7 +401,7 @@ def timeline(self, request, pk=None): source_images = list( qs.filter(timestamp__range=(start_time, end_time)) .order_by("timestamp") - .values("id", "timestamp", "detections_count") + .values("id", "timestamp", "detections_count", "was_processed") ) timeline = [] @@ -418,6 +418,7 @@ def timeline(self, request, pk=None): "captures_count": 0, "detections_count": 0, "detection_counts": [], + "was_processed": False, } while image_index < len(source_images) and source_images[image_index]["timestamp"] <= interval_end: @@ -435,6 +436,7 @@ def timeline(self, request, pk=None): # 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]) + interval_data["was_processed"] = image["was_processed"] timeline.append(interval_data) current_time = interval_end @@ -705,6 +707,7 @@ class SourceImageCollectionViewSet(DefaultViewSet, ProjectMixin): SourceImageCollection.objects.all() .with_source_images_count() # type: ignore .with_source_images_with_detections_count() + .with_source_images_processed_count() .prefetch_related("jobs") ) serializer_class = SourceImageCollectionSerializer @@ -720,6 +723,7 @@ class SourceImageCollectionViewSet(DefaultViewSet, ProjectMixin): "method", "source_images_count", "source_images_with_detections_count", + "source_images_processed_count", "occurrences_count", ] @@ -894,7 +898,9 @@ class DetectionViewSet(DefaultViewSet, ProjectMixin): API endpoint that allows detections to be viewed or edited. """ - queryset = Detection.objects.all().select_related("source_image", "detection_algorithm") + queryset = Detection.objects.exclude(Q(bbox__isnull=True) | Q(bbox=None) | Q(bbox=[])).select_related( + "source_image", "detection_algorithm" + ) serializer_class = DetectionSerializer filterset_fields = ["source_image", "detection_algorithm", "source_image__project"] ordering_fields = ["created_at", "updated_at", "detection_score", "timestamp"] diff --git a/ami/main/models.py b/ami/main/models.py index 515f5286a..3a12b324b 100644 --- a/ami/main/models.py +++ b/ami/main/models.py @@ -1656,6 +1656,17 @@ def with_taxa_count(self, project: Project | None = None, request=None): taxa_count=Coalesce(models.Subquery(taxa_subquery, output_field=models.IntegerField()), 0) ) + def with_was_processed(self): + """ + Annotate each SourceImage with a boolean `was_processed` indicating + whether any detections exist for that image. + + This mirrors `SourceImage.get_was_processed()` but as a queryset + annotation for efficient bulk queries. + """ + processed_exists = models.Exists(Detection.objects.filter(source_image_id=models.OuterRef("pk"))) + return self.annotate(was_processed=processed_exists) + class SourceImageManager(models.Manager.from_queryset(SourceImageQuerySet)): pass @@ -1755,7 +1766,15 @@ def size_display(self) -> str: return filesizeformat(self.size) def get_detections_count(self) -> int: - return self.detections.distinct().count() + # Detections count excludes detections without bounding boxes + # Detections with null bounding boxes are valid and indicates the image was successfully processed + return self.detections.exclude(Q(bbox__isnull=True) | Q(bbox=None) | Q(bbox=[])).count() + + def get_was_processed(self, algorithm_key: str | None = None) -> bool: + if algorithm_key: + return self.detections.filter(detection_algorithm__key=algorithm_key).exists() + else: + return self.detections.exists() def get_base_url(self) -> str | None: """ @@ -3603,7 +3622,23 @@ def with_source_images_count(self): def with_source_images_with_detections_count(self): return self.annotate( source_images_with_detections_count=models.Count( - "images", filter=models.Q(images__detections__isnull=False), distinct=True + "images", + filter=( + models.Q(images__detections__isnull=False) + & ~models.Q(images__detections__bbox__isnull=True) + & ~models.Q(images__detections__bbox=None) + & ~models.Q(images__detections__bbox=[]) + ), + distinct=True, + ) + ) + + def with_source_images_processed_count(self): + return self.annotate( + source_images_processed_count=models.Count( + "images", + filter=models.Q(images__detections__isnull=False), + distinct=True, ) ) @@ -3714,7 +3749,10 @@ def source_images_count(self) -> int | None: def source_images_with_detections_count(self) -> int | None: # This should always be pre-populated using queryset annotations - # return self.images.filter(detections__isnull=False).count() + return None + + def source_images_processed_count(self) -> int | None: + # This should always be pre-populated using queryset annotations return None def occurrences_count(self) -> int | None: diff --git a/ami/ml/tests.py b/ami/ml/tests.py index 4ef3ca9e4..13cbe1d76 100644 --- a/ami/ml/tests.py +++ b/ami/ml/tests.py @@ -691,20 +691,25 @@ def test_image_with_null_detection(self): """ Test saving results for a pipeline that returns null detections for some images. """ - results = self.fake_pipeline_results(self.test_images, self.pipeline) + image = self.test_images[0] + results = self.fake_pipeline_results([image], self.pipeline) # Manually change the results for a single image to a list of empty detections - first_image_id = results.source_images[0].id - new_detections = [detection for detection in results.detections if detection.source_image_id == first_image_id] - results.detections = new_detections + results.detections = [] save_results(results) - # After save is done, each image should have at least one detection, - # even if the detection list from the PS was empty. - for image in self.test_images: - image.save() - self.assertEqual(image.detections_count, 1) + image.save() + self.assertEqual(image.get_detections_count(), 0) # detections_count should exclude null detections + total_num_detections = image.detections.distinct().count() + self.assertEqual(total_num_detections, 1) + + was_processed = image.get_was_processed() + self.assertEqual(was_processed, True) + + # Also test filtering by algorithm + was_processed = image.get_was_processed(algorithm_key="random-detector") + self.assertEqual(was_processed, True) class TestAlgorithmCategoryMaps(TestCase): diff --git a/ui/src/data-services/models/collection.ts b/ui/src/data-services/models/collection.ts index e825614e3..81610bdc4 100644 --- a/ui/src/data-services/models/collection.ts +++ b/ui/src/data-services/models/collection.ts @@ -75,6 +75,10 @@ export class Collection extends Entity { return this._data.source_images_with_detections_count } + get numImagesProcessed(): number | undefined { + return this._data.source_images_processed_count + } + get numImagesWithDetectionsLabel(): string { const pct = this.numImagesWithDetections && this.numImages @@ -86,6 +90,16 @@ export class Collection extends Entity { )}%)` } + get numImagesProccessed(): string { + const numProcessed = this.numImagesProcessed ?? 0 + const pct = + this.numImages && this.numImages > 0 + ? (numProcessed / this.numImages) * 100 + : 0 + + return `${numProcessed.toLocaleString()} (${pct.toFixed(0)}%)` + } + get numJobs(): number | undefined { return this._data.jobs?.length } diff --git a/ui/src/data-services/models/timeline-tick.ts b/ui/src/data-services/models/timeline-tick.ts index 49d7553f3..1e432db65 100644 --- a/ui/src/data-services/models/timeline-tick.ts +++ b/ui/src/data-services/models/timeline-tick.ts @@ -10,6 +10,7 @@ export type ServerTimelineTick = { captures_count: number detections_count: number detections_avg: number + was_processed: boolean } export class TimelineTick { @@ -31,6 +32,10 @@ export class TimelineTick { return this._timelineTick.detections_avg ?? 0 } + get wasProcessed(): boolean { + return this._timelineTick.was_processed + } + get numCaptures(): number { return this._timelineTick.captures_count ?? 0 } diff --git a/ui/src/pages/project/collections/collection-columns.tsx b/ui/src/pages/project/collections/collection-columns.tsx index a3ee5018b..740f999d6 100644 --- a/ui/src/pages/project/collections/collection-columns.tsx +++ b/ui/src/pages/project/collections/collection-columns.tsx @@ -104,6 +104,16 @@ export const columns: (projectId: string) => TableColumn[] = ( ), }, + { + id: 'total-processed-captures', + name: translate(STRING.FIELD_LABEL_TOTAL_PROCESSED_CAPTURES), + styles: { + textAlign: TextAlign.Right, + }, + renderCell: (item: Collection) => ( + + ), + }, { id: 'occurrences', name: translate(STRING.FIELD_LABEL_OCCURRENCES), diff --git a/ui/src/pages/project/collections/collections.tsx b/ui/src/pages/project/collections/collections.tsx index efbcb59fd..0e4a579c6 100644 --- a/ui/src/pages/project/collections/collections.tsx +++ b/ui/src/pages/project/collections/collections.tsx @@ -28,6 +28,7 @@ export const Collections = () => { settings: true, captures: true, 'captures-with-detections': true, + 'total-processed-captures': true, status: true, } ) diff --git a/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx b/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx index 2b2c84811..19a53d034 100644 --- a/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx +++ b/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx @@ -8,6 +8,7 @@ import { useDynamicPlotWidth } from './useDynamicPlotWidth' const fontFamily = 'Mazzard, sans-serif' const lineColorCaptures = '#4E4F57' const lineColorDetections = '#5F8AC6' +const lineColorProcessed = '#FF0000' const spikeColor = '#FFFFFF' const textColor = '#303137' const tooltipBgColor = '#FFFFFF' @@ -67,6 +68,32 @@ const ActivityPlot = ({ name: 'Avg. detections', yaxis: 'y2', }, + { + x: timeline.map( + (timelineTick) => new Date(timelineTick.startDate) + ), + y: timeline.map((timelineTick) => + timelineTick.numCaptures > 0 + ? timelineTick.wasProcessed + ? 0 + : 1 + : 0 + ), + customdata: timeline.map((timelineTick) => + timelineTick.numCaptures > 0 + ? timelineTick.wasProcessed + ? 'Yes' + : 'No' + : 'N/A' + ), + hovertemplate: 'Was processed: %{customdata}', + fill: 'tozeroy', + type: 'scatter', + mode: 'lines', + line: { color: lineColorProcessed, width: 1 }, + name: 'Was processed', + yaxis: 'y2', + }, ]} layout={{ height: 100, diff --git a/ui/src/utils/language.ts b/ui/src/utils/language.ts index 7d27ca081..90c402ceb 100644 --- a/ui/src/utils/language.ts +++ b/ui/src/utils/language.ts @@ -140,6 +140,7 @@ export enum STRING { FIELD_LABEL_TIME_OBSERVED, FIELD_LABEL_TIMESTAMP, FIELD_LABEL_TOTAL_FILES, + FIELD_LABEL_TOTAL_PROCESSED_CAPTURES, FIELD_LABEL_TOTAL_SIZE, FIELD_LABEL_TRAINING_IMAGES, FIELD_LABEL_TYPE, @@ -409,6 +410,7 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { [STRING.FIELD_LABEL_TIME_OBSERVED]: 'Local time observed', [STRING.FIELD_LABEL_TIMESTAMP]: 'Timestamp', [STRING.FIELD_LABEL_TOTAL_FILES]: 'Total files', + [STRING.FIELD_LABEL_TOTAL_PROCESSED_CAPTURES]: 'Total Processed Captures', [STRING.FIELD_LABEL_TOTAL_SIZE]: 'Total size', [STRING.FIELD_LABEL_TRAINING_IMAGES]: 'Reference images', [STRING.FIELD_LABEL_TYPE]: 'Type', From 342c3d204e57e95eab95eede9b069867653efd28 Mon Sep 17 00:00:00 2001 From: Vanessa Mac Date: Wed, 18 Feb 2026 21:04:15 -0500 Subject: [PATCH 3/3] Address review comments --- ami/main/models.py | 3 +- ami/ml/models/pipeline.py | 81 ++++++++++++------- ui/src/data-services/models/collection.ts | 2 +- .../collections/collection-columns.tsx | 2 +- .../playback/activity-plot/activity-plot.tsx | 6 +- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/ami/main/models.py b/ami/main/models.py index f1b2c4bf2..e278c6db6 100644 --- a/ami/main/models.py +++ b/ami/main/models.py @@ -1838,7 +1838,7 @@ def size_display(self) -> str: def get_detections_count(self) -> int: # Detections count excludes detections without bounding boxes # Detections with null bounding boxes are valid and indicates the image was successfully processed - return self.detections.exclude(Q(bbox__isnull=True) | Q(bbox=None) | Q(bbox=[])).count() + return self.detections.exclude(Q(bbox__isnull=True) | Q(bbox=[])).count() def get_was_processed(self, algorithm_key: str | None = None) -> bool: if algorithm_key: @@ -2014,6 +2014,7 @@ def update_detection_counts(qs: models.QuerySet[SourceImage] | None = None, null subquery = models.Subquery( Detection.objects.filter(source_image_id=models.OuterRef("pk")) + .exclude(Q(bbox__isnull=True) | Q(bbox=[])) .values("source_image_id") .annotate(count=models.Count("id")) .values("count"), diff --git a/ami/ml/models/pipeline.py b/ami/ml/models/pipeline.py index 413178389..6334e865d 100644 --- a/ami/ml/models/pipeline.py +++ b/ami/ml/models/pipeline.py @@ -8,7 +8,6 @@ import collections import dataclasses -import datetime import logging import time import typing @@ -815,6 +814,50 @@ class PipelineSaveResults: total_time: float +def create_null_detections_for_undetected_images( + results: PipelineResultsResponse, + algorithms_known: dict[str, Algorithm], + logger: logging.Logger = logger, +) -> list[DetectionResponse]: + """ + Create null DetectionResponse objects (empty bbox) for images that have no detections. + + :param results: The PipelineResultsResponse from the processing service + :param algorithms_known: Dictionary of algorithms keyed by algorithm key + + :return: List of DetectionResponse objects with null bbox + """ + source_images_with_detections = {int(detection.source_image_id) for detection in results.detections} + null_detections_to_add = [] + + for source_img in results.source_images: + if int(source_img.id) not in source_images_with_detections: + detector_algorithm_reference = None + for known_algorithm in algorithms_known.values(): + if known_algorithm.task_type == AlgorithmTaskType.DETECTION: + detector_algorithm_reference = AlgorithmReference( + name=known_algorithm.name, key=known_algorithm.key + ) + + if detector_algorithm_reference is None: + logger.error( + f"Could not identify the detector algorithm. " + f"A null detection was not created for Source Image {source_img.id}" + ) + continue + + null_detections_to_add.append( + DetectionResponse( + source_image_id=source_img.id, + bbox=None, + algorithm=detector_algorithm_reference, + timestamp=now(), + ) + ) + + return null_detections_to_add + + @celery_app.task(soft_time_limit=60 * 4, time_limit=60 * 5) def save_results( results: PipelineResultsResponse | None = None, @@ -873,36 +916,12 @@ def save_results( # Ensure all images have detections # if not, add a NULL detection (empty bbox) to the results - source_images_with_detections = [detection.source_image_id for detection in results.detections] - source_images_with_detections = set(source_images_with_detections) - null_detections_to_add = [] - - for source_img in results.source_images: - if source_img.id not in source_images_with_detections: - detector_algorithm_reference = None - for known_algorithm in algorithms_known.values(): - if known_algorithm.task_type == AlgorithmTaskType.DETECTION: - detector_algorithm_reference = AlgorithmReference( - name=known_algorithm.name, key=known_algorithm.key - ) - - if detector_algorithm_reference is None: - job_logger.error( - f"Could not identify the detector algorithm. " - f"A null detection was not created for Source Image {source_img.id}" - ) - continue - - null_detections_to_add.append( - DetectionResponse( - source_image_id=source_img.id, - bbox=None, - algorithm=detector_algorithm_reference, - timestamp=datetime.datetime.now(), - ) - ) - - results.detections = results.detections + null_detections_to_add + null_detections = create_null_detections_for_undetected_images( + results=results, + algorithms_known=algorithms_known, + logger=job_logger, + ) + results.detections = results.detections + null_detections detections = create_detections( detections=results.detections, diff --git a/ui/src/data-services/models/collection.ts b/ui/src/data-services/models/collection.ts index 81610bdc4..b36203c2f 100644 --- a/ui/src/data-services/models/collection.ts +++ b/ui/src/data-services/models/collection.ts @@ -90,7 +90,7 @@ export class Collection extends Entity { )}%)` } - get numImagesProccessed(): string { + get numImagesProcessedLabel(): string { const numProcessed = this.numImagesProcessed ?? 0 const pct = this.numImages && this.numImages > 0 diff --git a/ui/src/pages/project/collections/collection-columns.tsx b/ui/src/pages/project/collections/collection-columns.tsx index 740f999d6..f6200179c 100644 --- a/ui/src/pages/project/collections/collection-columns.tsx +++ b/ui/src/pages/project/collections/collection-columns.tsx @@ -111,7 +111,7 @@ export const columns: (projectId: string) => TableColumn[] = ( textAlign: TextAlign.Right, }, renderCell: (item: Collection) => ( - + ), }, { diff --git a/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx b/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx index 19a53d034..9dad9e6e7 100644 --- a/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx +++ b/ui/src/pages/session-details/playback/activity-plot/activity-plot.tsx @@ -8,7 +8,7 @@ import { useDynamicPlotWidth } from './useDynamicPlotWidth' const fontFamily = 'Mazzard, sans-serif' const lineColorCaptures = '#4E4F57' const lineColorDetections = '#5F8AC6' -const lineColorProcessed = '#FF0000' +const lineColorProcessed = '#00ff1a' const spikeColor = '#FFFFFF' const textColor = '#303137' const tooltipBgColor = '#FFFFFF' @@ -75,8 +75,8 @@ const ActivityPlot = ({ y: timeline.map((timelineTick) => timelineTick.numCaptures > 0 ? timelineTick.wasProcessed - ? 0 - : 1 + ? timelineTick.numCaptures // Show number of captures so value is visible + : 0 : 0 ), customdata: timeline.map((timelineTick) =>