Skip to content

Commit

Permalink
#4537 - Ability to group annotations by layer
Browse files Browse the repository at this point in the history
- Add by-layer grouping mode
  • Loading branch information
reckart committed Feb 21, 2024
1 parent c4bbb01 commit f773714
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"properties": {
"mode": {
"type": "string",
"enum": ["by-label", "by-position"]
"enum": ["by-label", "by-position", "by-layer"]
},
"sortByScore": {
"type": "boolean"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<script lang="ts">
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AnnotatedText,
Annotation,
AnnotationOverEvent,
AnnotationOutEvent,
DiamAjax,
Relation,
Span,
Layer,
} from "@inception-project/inception-js-api";
import { compareOffsets } from "@inception-project/inception-js-api/src/model/Offsets";
import LabelBadge from "./LabelBadge.svelte";
import SpanText from "./SpanText.svelte";
import { compareSpanText, groupBy, uniqueLayers } from "./Utils";
import { sortByScore, recommendationsFirst } from "./AnnotationBrowserState"
export let ajaxClient: DiamAjax;
export let data: AnnotatedText;
let groupedAnnotations: Record<string, Annotation[]>;
let sortedLayers: Layer[];
$: {
sortedLayers = uniqueLayers(data)
const relations = data?.relations.values() || []
const spans = data?.spans.values() || []
groupedAnnotations = groupBy(
[...spans, ...relations],
(s) => s.layer.name
)
for (const items of Object.values(groupedAnnotations)) {
items.sort((a, b) => {
if (a instanceof Span && !(b instanceof Span)) {
return -1;
}
if (a instanceof Relation && !(b instanceof Relation)) {
return 1;
}
const aIsRec = a.vid.toString().startsWith("rec:")
const bIsRec = b.vid.toString().startsWith("rec:")
if ($sortByScore && aIsRec && !bIsRec) {
return $recommendationsFirst ? -1 : 1;
}
if (a instanceof Span && b instanceof Span) {
if ($sortByScore && aIsRec && bIsRec) {
return b.score - a.score;
}
return (
compareSpanText(data, a, b) ||
compareOffsets(a.offsets[0], b.offsets[0])
);
}
if (a instanceof Relation && b instanceof Relation) {
if ($sortByScore && aIsRec && bIsRec) {
return b.score - a.score;
}
return compareOffsets(
(a.arguments[0].target as Span).offsets[0],
(b.arguments[0].target as Span).offsets[0]
);
}
console.error("Unexpected annotation type combination", a, b);
});
}
}
function scrollTo(ann: Annotation) {
ajaxClient.scrollTo({ id: ann.vid });
}
function mouseOverAnnotation(event: MouseEvent, annotation: Annotation) {
event.target.dispatchEvent(new AnnotationOverEvent(annotation, event))
}
function mouseOutAnnotation(event: MouseEvent, annotation: Annotation) {
event.target.dispatchEvent(new AnnotationOutEvent(annotation, event))
}
</script>

{#if !data}
<div class="m-auto d-flex flex-column justify-content-center">
<div class="d-flex flex-row justify-content-center">
<div class="spinner-border text-muted" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
{:else}
<div class="d-flex flex-column">
<div class="form-check form-switch mx-2">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="sortByScore"
bind:checked={$sortByScore}
/>
<label class="form-check-label" for="sortByScore"
>Sort by score</label
>
</div>
<div class="form-check form-switch mx-2" class:d-none={!$sortByScore}>
<input
class="form-check-input"
type="checkbox"
role="switch"
id="recommendationsFirst"
bind:checked={$recommendationsFirst}
/>
<label class="form-check-label" for="recommendationsFirst"
>Recommendations first</label
>
</div>
</div>
<div class="flex-content fit-child-snug">
{#if sortedLayers || sortedLayers?.length}
<ul class="scrolling flex-content list-group list-group-flush">
{#each sortedLayers as layer}
<li class="list-group-item py-0 px-0 border-0">
<div
class="px-2 py-1 bg-light-subtle fw-bold sticky-top border-top border-bottom"
>
{layer.name}
</div>
<ul class="px-0 list-group list-group-flush">
{#if groupedAnnotations[layer.name]}
{#each groupedAnnotations[layer.name] as ann}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<li
class="list-group-item list-group-item-action p-0 d-flex"
on:mouseover={ev => mouseOverAnnotation(ev, ann)}
on:mouseout={ev => mouseOutAnnotation(ev, ann)}
>
<div
class="text-secondary bg-light-subtle border-end px-2 d-flex align-items-center"
>
{#if ann instanceof Span}
<div class="annotation-type-marker i7n-icon-span"/>
{:else if ann instanceof Relation}
<div class="annotation-type-marker i7n-icon-relation"/>
{/if}
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-grow-1 my-1 mx-2 position-relative overflow-hidden"
on:click={() => scrollTo(ann)}
>
<div class="float-end labels">
<LabelBadge
annotation={ann}
{ajaxClient}
showText={true}
/>
</div>

{#if ann instanceof Span}
<SpanText {data} span={ann} />
{:else if ann instanceof Relation}
<SpanText
{data}
span={ann.arguments[0].target}
/>
{/if}
</div>
</li>
{/each}
{:else}
<li class="list-group-item list-group-item-action p-2 text-center text-secondary bg-light">
No occurrences
</li>
{/if}
</ul>
</li>
{/each}
</ul>
{/if}
</div>
{/if}

<style lang="scss">
.labels {
background: linear-gradient(to right, transparent 0px, var(--bs-body-bg) 15px);
padding-left: 20px;
z-index: 10;
position: relative;
}
.annotation-type-marker {
width: 1em;
text-align: center;
}
.list-group-flush > .list-group-item:last-child {
border-bottom-width: 1px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
} from "./AnnotationBrowserState";
import AnnotationsByPositionList from "./AnnotationsByPositionList.svelte";
import AnnotationsByLabelList from "./AnnotationsByLabelList.svelte";
import AnnotationsByLayerList from "./AnnotationsByLayerList.svelte";
import AnnotationDetailPopOver from "@inception-project/inception-js-api/src/widget/AnnotationDetailPopOver.svelte"
export let wsEndpointUrl: string;
Expand All @@ -52,6 +53,7 @@
let modes = {
"by-position": "Group by position",
"by-label": "Group by label",
"by-layer": "Group by layer",
};
let tooManyAnnotations = false;
Expand Down Expand Up @@ -154,6 +156,8 @@
</div>
{:else if $groupingMode == "by-position"}
<AnnotationsByPositionList {ajaxClient} {data} />
{:else if $groupingMode == "by-layer"}
<AnnotationsByLayerList {ajaxClient} {data} />
{:else}
<AnnotationsByLabelList {ajaxClient} {data} {pinnedGroups} />
{/if}
Expand Down
18 changes: 17 additions & 1 deletion inception/inception-diam-editor/src/main/ts/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AnnotatedText, Annotation, Offsets, Relation, Span, VID } from '@inception-project/inception-js-api'
import { AnnotatedText, Annotation, Layer, Offsets, Relation, Span, VID } from '@inception-project/inception-js-api'
import { compareOffsets } from '@inception-project/inception-js-api/src/model/Offsets'

export function renderLabel (ann?: Annotation): string {
Expand All @@ -29,6 +29,22 @@ export function renderLabel (ann?: Annotation): string {
return label
}

export function uniqueLayers (data: AnnotatedText): Layer[] {
if (!data) return []

const sortedLayersWithDuplicates = Array.from(data.annotations(), (ann) => ann.layer)
.sort((a, b) => a.name.localeCompare(b.name, undefined, { usage: 'sort', sensitivity: 'variant' }))

const sortedLayers: Layer[] = []
for (let i = 0; i < sortedLayersWithDuplicates.length; i++) {
if (i === 0 || sortedLayersWithDuplicates[i - 1] !== sortedLayersWithDuplicates[i]) {
sortedLayers.push(sortedLayersWithDuplicates[i])
}
}

return sortedLayers
}

export function uniqueLabels (data: AnnotatedText): string[] {
if (!data) return []

Expand Down

0 comments on commit f773714

Please sign in to comment.