Skip to content

Commit d10b304

Browse files
fix annotation list class ordering
1 parent 50515ba commit d10b304

5 files changed

Lines changed: 94 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented here.
44

55
## [unreleased]
66

7+
## [0.23.6] - May 11th, 2026
8+
- Fix `AnnotationList` class ordering to match the `AnnotationID` toolbox item ordering, which is based on the order of annotations in the subtask's annotation array.
9+
- Add local storage of checkbox options for the `AnnotationList` toolbox item
10+
711
## [0.23.5] - May 6th, 2026
812
- Fix bug where on some browsers, middle-click-drag when annotations were vanished would trigger auto-scroll rather than the normal pan behavior.
913

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ulabel",
33
"description": "An image annotation tool.",
4-
"version": "0.23.5",
4+
"version": "0.23.6",
55
"main": "dist/ulabel.min.js",
66
"module": "dist/ulabel.min.js",
77
"types": "index.d.ts",

src/toolbox_items/annotation_list.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,14 @@ export class AnnotationListToolboxItem extends ToolboxItem {
272272
// Show/hide deprecated annotations checkbox
273273
$(document).on("change.ulabel", "#annotation-list-show-deprecated", (e) => {
274274
this.show_deprecated = (e.target as HTMLInputElement).checked;
275+
set_local_storage_item("ulabel_annotation_list_show_deprecated", this.show_deprecated ? "true" : "false");
275276
this.update_list();
276277
});
277278

278279
// Group by class checkbox
279280
$(document).on("change.ulabel", "#annotation-list-group-by-class", (e) => {
280281
this.group_by_class = (e.target as HTMLInputElement).checked;
282+
set_local_storage_item("ulabel_annotation_list_group_by_class", this.group_by_class ? "true" : "false");
281283
this.update_list();
282284
});
283285

@@ -448,11 +450,30 @@ export class AnnotationListToolboxItem extends ToolboxItem {
448450
groups[class_id].push(annotation);
449451
}
450452

453+
// Build the render order: first walk class_defs in declared order so the
454+
// group headers appear in the same order as the AnnotationIDToolboxItem.
455+
// Then append any orphan class_ids (not in class_defs) as a defensive
456+
// fallback so we never silently drop annotations.
457+
const ordered_class_ids: number[] = [];
458+
const seen_class_ids: Set<number> = new Set();
459+
for (const def of subtask.class_defs) {
460+
if (groups[def.id]) {
461+
ordered_class_ids.push(def.id);
462+
seen_class_ids.add(def.id);
463+
}
464+
}
465+
const orphan_class_ids = Object.keys(groups)
466+
.map((id_str) => parseInt(id_str))
467+
.filter((id) => !seen_class_ids.has(id))
468+
.sort((a, b) => a - b);
469+
for (const id of orphan_class_ids) {
470+
ordered_class_ids.push(id);
471+
}
472+
451473
// Build HTML for each group
452474
let html = "";
453475

454-
for (const class_id_str in groups) {
455-
const class_id = parseInt(class_id_str);
476+
for (const class_id of ordered_class_ids) {
456477
const group_annotations = groups[class_id];
457478
const class_def = subtask.class_defs.find((def) => def.id === class_id);
458479
const class_name = class_def ? class_def.name : "Unknown";
@@ -604,23 +625,43 @@ export class AnnotationListToolboxItem extends ToolboxItem {
604625
* Code called after all of ULabel's constructor and initialization code is called
605626
*/
606627
public after_init(): void {
607-
// Restore collapsed state from localStorage
608-
this.restore_collapsed_state();
628+
// Restore persisted UI state from localStorage
629+
this.restore_persisted_state();
609630

610631
// Initial list update
611632
this.update_list();
612633
}
613634

614635
/**
615-
* Restore the collapsed state from localStorage
636+
* Restore persisted UI state (collapsed flag + checkbox toggles) from localStorage.
616637
*/
617-
private restore_collapsed_state(): void {
618-
const stored_state = get_local_storage_item("ulabel_annotation_list_collapsed");
619-
if (stored_state === "false") {
638+
private restore_persisted_state(): void {
639+
const stored_collapsed = get_local_storage_item("ulabel_annotation_list_collapsed");
640+
if (stored_collapsed === "false") {
620641
this.is_collapsed = false;
621-
} else if (stored_state === "true") {
642+
} else if (stored_collapsed === "true") {
622643
this.is_collapsed = true;
623644
}
645+
646+
const stored_show_deprecated = get_local_storage_item("ulabel_annotation_list_show_deprecated");
647+
if (stored_show_deprecated === "true") {
648+
this.show_deprecated = true;
649+
} else if (stored_show_deprecated === "false") {
650+
this.show_deprecated = false;
651+
}
652+
653+
const stored_group_by_class = get_local_storage_item("ulabel_annotation_list_group_by_class");
654+
if (stored_group_by_class === "true") {
655+
this.group_by_class = true;
656+
} else if (stored_group_by_class === "false") {
657+
this.group_by_class = false;
658+
}
659+
660+
// Reflect restored values into the checkbox DOM so the UI matches state.
661+
const show_deprecated_input = document.querySelector<HTMLInputElement>("#annotation-list-show-deprecated");
662+
if (show_deprecated_input) show_deprecated_input.checked = this.show_deprecated;
663+
const group_by_class_input = document.querySelector<HTMLInputElement>("#annotation-list-group-by-class");
664+
if (group_by_class_input) group_by_class_input.checked = this.group_by_class;
624665
}
625666

626667
/**

src/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const ULABEL_VERSION = "0.23.5";
1+
export const ULABEL_VERSION = "0.23.6";

tests/e2e/annotation_list.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,42 @@ test.describe("Annotation List Grouping", () => {
121121
expect(await header.count()).toBeGreaterThan(0);
122122
}
123123
});
124+
125+
test("class group headers appear in class_defs order, not numeric class id order", async ({ page }) => {
126+
await wait_for_ulabel_init(page);
127+
128+
// multi-class.html defines class_defs as [Sedan(10), SUV(11), Truck(12)].
129+
// Draw two annotations: one with the default class (Sedan), one after
130+
// switching to the third class (Truck). SUV is intentionally left empty.
131+
await draw_bbox(page, [100, 100], [200, 200]);
132+
await page.mouse.move(300, 300);
133+
await page.keyboard.press("3");
134+
await draw_bbox(page, [300, 300], [400, 400]);
135+
136+
// Enable group-by-class and confirm the natural ordering matches class_defs
137+
// (Sedan before Truck).
138+
await toggle_group_by_class(page, true);
139+
let header_texts = await page.locator(".annotation-list-class-group-header").allTextContents();
140+
// headers contain the class name plus a count like "(1)"; strip whitespace
141+
let names_in_order = header_texts.map((t) => t.trim().split(/\s+/)[0]);
142+
expect(names_in_order).toEqual(["Sedan", "Truck"]);
143+
144+
// Now reverse class_defs at runtime. With the old (buggy) implementation
145+
// the headers would still appear in numeric class_id order
146+
// ([Sedan(10), Truck(12)]). With the fix they should match the new
147+
// class_defs order ([Truck, Sedan]).
148+
await page.evaluate(() => {
149+
const subtask = window.ulabel.subtasks[window.ulabel.state.current_subtask];
150+
subtask.class_defs.reverse();
151+
});
152+
153+
// Force the list to re-render. Toggling the checkbox off + on triggers
154+
// build_grouped_list_html.
155+
await toggle_group_by_class(page, false);
156+
await toggle_group_by_class(page, true);
157+
158+
header_texts = await page.locator(".annotation-list-class-group-header").allTextContents();
159+
names_in_order = header_texts.map((t) => t.trim().split(/\s+/)[0]);
160+
expect(names_in_order).toEqual(["Truck", "Sedan"]);
161+
});
124162
});

0 commit comments

Comments
 (0)