mirror of https://github.com/doccano/doccano.git
pythonannotation-tooldatasetsactive-learningtext-annotationdatasetnatural-language-processingdata-labelingmachine-learning
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
501 lines
13 KiB
501 lines
13 KiB
<template>
|
|
<div id="connections-wrapper">
|
|
<div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
|
|
<entity-item
|
|
v-for="(chunk, i) in chunks"
|
|
:key="i"
|
|
:spanid="chunk.id"
|
|
:content="chunk.text"
|
|
:newline="chunk.newline"
|
|
:label="chunk.label"
|
|
:color="chunk.color"
|
|
:labels="labels"
|
|
:link-types="linkTypes"
|
|
:source-chunk="sourceChunk"
|
|
:source-link-type="sourceLinkType"
|
|
@remove="deleteAnnotation(chunk.id)"
|
|
@update="updateEntity($event.id, chunk.id)"
|
|
@selectSource="selectSource(chunk)"
|
|
@selectTarget="selectTarget(chunk)"
|
|
@deleteLink="deleteLink($event.id, $event.ndx)"
|
|
@selectNewLinkType="selectNewLinkType($event)"
|
|
@hideAllLinkMenus="hideAllLinkMenus()"
|
|
/>
|
|
<v-menu
|
|
v-model="showMenu"
|
|
:position-x="x"
|
|
:position-y="y"
|
|
absolute
|
|
offset-y
|
|
>
|
|
<v-list
|
|
dense
|
|
min-width="150"
|
|
max-height="400"
|
|
class="overflow-y-auto"
|
|
>
|
|
<v-list-item
|
|
v-for="(label, i) in labels"
|
|
:key="i"
|
|
v-shortkey="[label.suffixKey]"
|
|
@shortkey="assignLabel(label.id)"
|
|
@click="assignLabel(label.id)"
|
|
>
|
|
<v-list-item-content>
|
|
<v-list-item-title v-text="label.text"/>
|
|
</v-list-item-content>
|
|
<v-list-item-action>
|
|
<v-list-item-action-text v-text="label.suffixKey"/>
|
|
</v-list-item-action>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</div>
|
|
|
|
<canvas id="connections">
|
|
</canvas>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import EntityItem from './EntityItem'
|
|
|
|
export default {
|
|
components: {
|
|
EntityItem
|
|
},
|
|
|
|
props: {
|
|
text: {
|
|
type: String,
|
|
default: '',
|
|
required: true
|
|
},
|
|
labels: {
|
|
type: Array,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
linkTypes: {
|
|
type: Array,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
entities: {
|
|
type: Array,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
deleteAnnotation: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
updateEntity: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
addEntity: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
sourceChunk: {
|
|
type: Object,
|
|
default: () => {
|
|
},
|
|
required: true
|
|
},
|
|
sourceLinkType: {
|
|
type: Object,
|
|
default: () => {
|
|
},
|
|
required: true
|
|
},
|
|
selectSource: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
selectTarget: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
deleteLink: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
selectNewLinkType: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
},
|
|
hideAllLinkMenus: {
|
|
type: Function,
|
|
default: () => ([]),
|
|
required: true
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
showMenu: false,
|
|
x: 0,
|
|
y: 0,
|
|
start: 0,
|
|
end: 0
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
sortedEntities() {
|
|
return this.entities.slice().sort((a, b) => a.startOffset - b.startOffset)
|
|
},
|
|
|
|
chunks() {
|
|
let chunks = []
|
|
let startOffset = 0
|
|
// to count the number of characters correctly.
|
|
const characters = [...this.text]
|
|
for (const entity of this.sortedEntities) {
|
|
// add non-entities to chunks.
|
|
let piece = characters.slice(startOffset, entity.startOffset).join('')
|
|
chunks = chunks.concat(this.makeChunks(piece))
|
|
startOffset = entity.endOffset
|
|
// add entities to chunks.
|
|
const label = this.labelObject[entity.label]
|
|
piece = characters.slice(entity.startOffset, entity.endOffset).join('')
|
|
chunks.push({
|
|
id: entity.id,
|
|
label: label.text,
|
|
color: label.backgroundColor,
|
|
text: piece,
|
|
selectedAsLinkSource: false,
|
|
links: entity.links ? entity.links.map(link => {
|
|
return {
|
|
id: link.id,
|
|
type: link.type,
|
|
color: this.getColor(link.type),
|
|
targetId: link.annotation_id_2,
|
|
targetLabel: null // target label can be computed only after all chunks are made, see line 204
|
|
}
|
|
}) : null
|
|
})
|
|
}
|
|
// add the rest of text.
|
|
chunks = chunks.concat(this.makeChunks(characters.slice(startOffset, characters.length).join('')));
|
|
|
|
// populate the links. Must be done after chunk creation
|
|
chunks.forEach(chunk => {
|
|
if (chunk.links) {
|
|
chunk.links.forEach(link => {
|
|
link.targetLabel = chunks.find(target => target.id === link.targetId).text;
|
|
});
|
|
}
|
|
});
|
|
|
|
return chunks;
|
|
},
|
|
|
|
labelObject() {
|
|
const obj = {}
|
|
for (const label of this.labels) {
|
|
obj[label.id] = label
|
|
}
|
|
return obj
|
|
}
|
|
},
|
|
|
|
updated() {
|
|
this.$nextTick(() => {
|
|
const parentPos = document.getElementById('connections-wrapper').getBoundingClientRect();
|
|
const canvas = document.getElementById('connections');
|
|
canvas.width = parentPos.width;
|
|
canvas.height = parentPos.height;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.clearRect(0, 0, parentPos.width, parentPos.height);
|
|
|
|
const topPoints = this.drawnCountPoints(this.chunks.length);
|
|
const bottomPoints = this.drawnCountPoints(this.chunks.length);
|
|
|
|
const chunks = this.chunks;
|
|
|
|
chunks.forEach(function(sourceChunk, sourceNdx) {
|
|
if (sourceChunk.links) {
|
|
sourceChunk.links.forEach(function(link) {
|
|
let childPos = document.getElementById('spn-' + sourceChunk.id).getBoundingClientRect();
|
|
const y1 = childPos.y - parentPos.y;
|
|
|
|
childPos = document.getElementById('spn-' + link.targetId).getBoundingClientRect();
|
|
const y2 = childPos.y - parentPos.y;
|
|
|
|
const targetNdx = chunks.findIndex(ch => ch.id === link.targetId);
|
|
|
|
if (y1 < y2) {
|
|
bottomPoints[sourceNdx].count++;
|
|
topPoints[targetNdx].count++;
|
|
|
|
} else if (y1 > y2) {
|
|
topPoints[sourceNdx].count++;
|
|
bottomPoints[targetNdx].count++;
|
|
|
|
} else {
|
|
bottomPoints[sourceNdx].count++;
|
|
bottomPoints[targetNdx].count++;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
chunks.forEach(function(sourceChunk, sourceNdx) {
|
|
if (sourceChunk.links) {
|
|
sourceChunk.links.forEach(function(link) {
|
|
const sourcePos = document.getElementById('spn-' + sourceChunk.id).getBoundingClientRect();
|
|
let x1 = sourcePos.x - parentPos.x;
|
|
let y1 = sourcePos.y - parentPos.y;
|
|
|
|
const targetPos = document.getElementById('spn-' + link.targetId).getBoundingClientRect();
|
|
let x2 = targetPos.x - parentPos.x;
|
|
let y2 = targetPos.y - parentPos.y;
|
|
|
|
const targetNdx = chunks.findIndex(ch => ch.id === link.targetId);
|
|
|
|
ctx.beginPath();
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = link.color;
|
|
|
|
if (y1 < y2) {
|
|
bottomPoints[sourceNdx].drawn++;
|
|
topPoints[targetNdx].drawn++;
|
|
|
|
x1 += bottomPoints[sourceNdx].drawn * sourcePos.width / (bottomPoints[sourceNdx].count + 1);
|
|
y1 += sourcePos.height;
|
|
|
|
x2 += topPoints[targetNdx].drawn * targetPos.width / (topPoints[targetNdx].count + 1);
|
|
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x1, y1 + 12);
|
|
ctx.lineTo(x2, y2 - 12);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = link.color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x2, y2);
|
|
ctx.lineTo(x2 - 3, y2 - 5);
|
|
ctx.lineTo(x2 + 3, y2 - 5);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
} else if (y1 > y2) {
|
|
topPoints[sourceNdx].drawn++;
|
|
bottomPoints[targetNdx].drawn++;
|
|
|
|
x1 += topPoints[sourceNdx].drawn * sourcePos.width / (topPoints[sourceNdx].count + 1);
|
|
|
|
x2 += bottomPoints[targetNdx].drawn * targetPos.width / (bottomPoints[targetNdx].count + 1);
|
|
y2 += targetPos.height;
|
|
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x1, y1 - 12);
|
|
ctx.lineTo(x2, y2 + 12);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = link.color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x2, y2);
|
|
ctx.lineTo(x2 - 3, y2 + 5);
|
|
ctx.lineTo(x2 + 3, y2 + 5);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
} else {
|
|
bottomPoints[sourceNdx].drawn++;
|
|
bottomPoints[targetNdx].drawn++;
|
|
|
|
x1 += bottomPoints[sourceNdx].drawn * sourcePos.width / (bottomPoints[sourceNdx].count + 1);
|
|
y1 += sourcePos.height;
|
|
|
|
x2 += bottomPoints[targetNdx].drawn * targetPos.width / (bottomPoints[targetNdx].count + 1);
|
|
y2 += targetPos.height;
|
|
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x1, y1 + 12);
|
|
ctx.lineTo(x2, y2 + 12);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = link.color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x2, y2);
|
|
ctx.lineTo(x2 - 3, y2 + 5);
|
|
ctx.lineTo(x2 + 3, y2 + 5);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
makeChunks(text) {
|
|
const chunks = []
|
|
const snippets = text.split('\n')
|
|
for (const snippet of snippets.slice(0, -1)) {
|
|
chunks.push({
|
|
label: null,
|
|
color: null,
|
|
text: snippet + '\n',
|
|
newline: false
|
|
})
|
|
chunks.push({
|
|
label: null,
|
|
color: null,
|
|
text: '',
|
|
newline: true
|
|
})
|
|
}
|
|
chunks.push({
|
|
label: null,
|
|
color: null,
|
|
text: snippets.slice(-1)[0],
|
|
newline: false
|
|
})
|
|
return chunks
|
|
},
|
|
|
|
show(e) {
|
|
e.preventDefault()
|
|
this.showMenu = false
|
|
this.x = e.clientX || e.changedTouches[0].clientX
|
|
this.y = e.clientY || e.changedTouches[0].clientY
|
|
this.$nextTick(() => {
|
|
this.showMenu = true
|
|
})
|
|
},
|
|
|
|
setSpanInfo() {
|
|
let selection
|
|
// Modern browsers.
|
|
if (window.getSelection) {
|
|
selection = window.getSelection()
|
|
} else if (document.selection) {
|
|
selection = document.selection
|
|
}
|
|
// If nothing is selected.
|
|
if (selection.rangeCount <= 0) {
|
|
return
|
|
}
|
|
const range = selection.getRangeAt(0)
|
|
const preSelectionRange = range.cloneRange()
|
|
preSelectionRange.selectNodeContents(this.$el)
|
|
preSelectionRange.setEnd(range.startContainer, range.startOffset)
|
|
this.start = [...preSelectionRange.toString()].length
|
|
this.end = this.start + [...range.toString()].length
|
|
},
|
|
|
|
validateSpan() {
|
|
if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
|
|
return false
|
|
}
|
|
if (this.start === this.end) {
|
|
return false
|
|
}
|
|
for (const entity of this.entities) {
|
|
if ((entity.startOffset <= this.start) && (this.start < entity.endOffset)) {
|
|
return false
|
|
}
|
|
if ((entity.startOffset < this.end) && (this.end <= entity.endOffset)) {
|
|
return false
|
|
}
|
|
if ((this.start < entity.startOffset) && (entity.endOffset < this.end)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
|
|
open(e) {
|
|
this.$emit('hideAllLinkMenus');
|
|
|
|
this.setSpanInfo()
|
|
if (this.validateSpan()) {
|
|
this.show(e)
|
|
}
|
|
},
|
|
|
|
assignLabel(labelId) {
|
|
if (this.validateSpan()) {
|
|
this.addEntity(this.start, this.end, labelId)
|
|
this.showMenu = false
|
|
this.start = 0
|
|
this.end = 0
|
|
}
|
|
},
|
|
|
|
getColor(typeId) {
|
|
const type = this.linkTypes.find(type => type.id === typeId);
|
|
if (type) {
|
|
return type.color;
|
|
}
|
|
return "#787878";
|
|
},
|
|
|
|
drawnCountPoints(size) {
|
|
const points = Array(size);
|
|
|
|
for (let i = 0; i < points.length; i++) {
|
|
points[i] = {
|
|
drawn: 0,
|
|
count: 0
|
|
}
|
|
}
|
|
|
|
return points;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.highlight-container.highlight-container--bottom-labels {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.highlight-container {
|
|
line-height: 70px !important;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
white-space: pre-wrap;
|
|
cursor: default;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.highlight-container.highlight-container--bottom-labels .highlight.bottom {
|
|
margin-top: 6px;
|
|
}
|
|
|
|
#connections-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
#connections-wrapper canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
</style>
|