16 changed files with 2056 additions and 202 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,294 @@
|
||||
<template lang="pug"> |
||||
aside.col-retrieval--main |
||||
header.col-retrieval--main__header |
||||
h3 {{ filteredAnnotations.length }} Annotations |
||||
c-toolbar( |
||||
:unit="unit" :tag="tag" :pattern="pattern" :annotations="annotations" |
||||
:loi="loi" :activeAnnotation="activeAnnotation" |
||||
@removeSort="removeSort" @search="needle = $event" |
||||
) |
||||
hr |
||||
c-filter(:annotations="annotations" :openWith="openWith" @select="subset = $event" @openedWith="$emit('openedWith')") |
||||
transition-group(ref="list" name="flip-list" tag="ul" class="annotations-list") |
||||
li.annotations-list__item( |
||||
v-for="annotation in pagedAnnotations" :key="annotation.meta.timestamp" |
||||
:class="{ linked: isChild(annotation) || isParent(annotation) || (annotation === activeAnnotation && (activeParent || answersToActive.length)), child: isChild(annotation), parent: isParent(annotation) }" |
||||
) |
||||
c-card( |
||||
:annotation="annotation" :activeAnnotation="activeAnnotation" :filter="true" :replyTo="replyTo" |
||||
@highlight="$emit('highlight', $event)" @selectAnnotation="selectAnnotation" :replies="getReplies(annotation)" |
||||
@tag="sortByTag" @unit="sortByUnit" @pattern="sortByPattern" @loi="sortByLoi" |
||||
@avatar="$emit('avatar', $event)" @reply="$emit('reply', $event)" |
||||
@endorse="$emit('endorse', $event)" @funny="$emit('funny', $event)" |
||||
) |
||||
template(v-slot:extension) |
||||
slot(name="card") |
||||
transition(name="fade"): button.more.btn.primary(v-show="numPages < pages" @click="numPages += 1") show more |
||||
</template> |
||||
|
||||
<script> |
||||
import CToolbar from '@/components/_retrieval/CToolbar.vue'; |
||||
import CFilter from '@/components/_retrieval/CFilter.vue'; |
||||
import CCard from '@/components/Retrieval/CCard.vue'; |
||||
|
||||
export default { |
||||
components: { CToolbar, CFilter, CCard }, |
||||
props: { |
||||
/** The list of prior annotations */ |
||||
annotations: { type: Array, default: () => [] }, |
||||
openWith: { type: Object } |
||||
}, |
||||
data () { |
||||
return { |
||||
open: false, |
||||
dragging: false, |
||||
initialX: null, |
||||
width: 0, |
||||
eventListeners: [], |
||||
unit: null, |
||||
tag: null, |
||||
loi: null, |
||||
pattern: null, |
||||
needle: null, |
||||
aByPage: 10, |
||||
numPages: 1, |
||||
answersToActive: [], |
||||
activeParent: null, |
||||
activeAnnotation: null, |
||||
subset: null |
||||
} |
||||
}, |
||||
methods: { |
||||
sortByTag (tag) { |
||||
this.tag = this.tag === tag ? null : tag; |
||||
}, |
||||
sortByLoi (loi) { |
||||
this.loi = this.loi === loi ? null : loi; |
||||
}, |
||||
sortByPattern (pattern) { |
||||
this.pattern = this.pattern === pattern ? null : pattern; |
||||
}, |
||||
sortByUnit (unit) { |
||||
this.unit = this.unit?.title === unit?.title ? null : unit; |
||||
}, |
||||
removeSort (variable) { |
||||
if (variable === 'selectAnnotation') this.activeAnnotation = null |
||||
else this[variable] = null; |
||||
}, |
||||
isParent (annotation) { |
||||
return annotation.id === (this.activeParent?.id || this.activeParent); |
||||
}, |
||||
isChild (annotation) { |
||||
return this.answersToActive?.map(a => a.id).includes(annotation.id); |
||||
}, |
||||
selectAnnotation (annotation) { |
||||
if (!annotation) return this.activeAnnotation = null; |
||||
if (!this.annotations.includes(annotation)) { |
||||
annotation = this.annotations.filter(a => a.id === annotation.id)[0]; // ToDo: Find another way |
||||
} |
||||
if (this.activeAnnotation === annotation) this.activeAnnotation = null; |
||||
else this.activeAnnotation = annotation; |
||||
document.querySelector('.annotations-list').scrollIntoView({ behavior: 'smooth' }); |
||||
}, |
||||
getReplies (annotation) { |
||||
return this.annotations.filter(a => a.replyTo?.id === annotation.id || a.replyTo === annotation.id) |
||||
} |
||||
}, |
||||
computed: { |
||||
pages () { return Math.ceil(this.filteredAnnotations.length / this.aByPage); }, |
||||
subsetAnnotations () { |
||||
if (!this.subset) return this.annotations; |
||||
else return this.subset.annotations; |
||||
}, |
||||
filteredAnnotations () { |
||||
if (!this.needle || this.needle.length < 3) return this.subsetAnnotations; |
||||
else { |
||||
const n = this.needle.toLowerCase(); |
||||
return this.subsetAnnotations.filter(a => { |
||||
let haystacks = []; |
||||
let valid = false; |
||||
if (a.rawAnnotation) haystacks.push(a.rawAnnotation.text); |
||||
if (a.text) haystacks.push(a.text); |
||||
if (a.dataUnits) { |
||||
a.dataUnits.forEach(unit => { |
||||
if (!unit.aggregated) haystacks.push(unit.title); |
||||
else haystacks.push(...unit.units.map(subunit => subunit.title)) |
||||
}); |
||||
} |
||||
if (a.social?.author?.name) haystacks.push(a.social.author.name); |
||||
if (a.tags?.length) haystacks.push(...a.tags.map(t => t.name || t)); |
||||
|
||||
haystacks = haystacks.filter(h => h); |
||||
|
||||
return haystacks.reduce( |
||||
(valid, haystack) => haystack.toLowerCase().includes(n) ? true : valid, |
||||
valid |
||||
); |
||||
}); |
||||
} |
||||
}, |
||||
sortedAnnotations () { |
||||
const clone = [...this.filteredAnnotations]; |
||||
const sortFns = []; |
||||
|
||||
// Active annotation on top |
||||
sortFns.push((a, b) => a.id === (this.activeParent?.id || this.activeParent) ? -1 : b.id === (this.activeParent?.id || this.activeParent) ? 1 : 0 ); |
||||
sortFns.push((a, b) => a === this.activeAnnotation ? -1 : b === this.activeAnnotation ? 1 : 0); |
||||
sortFns.push((a, b) => this.answersToActive?.map(annotation => annotation.id).includes(a.id) ? -1 : this.answersToActive?.map(annotation => annotation.id).includes(b.id) ? 1 : 0 ); |
||||
|
||||
// Then sort by level of interpretation |
||||
if (this.loi) sortFns.push((a, b) => { |
||||
const levels = this.loi.split(' '); |
||||
function hasLevels (annotation, levels) { |
||||
let valid = true; |
||||
levels.forEach(l => valid = a.loi?.[l] ? valid : false); |
||||
return valid; |
||||
} |
||||
if (hasLevels(a, levels) && !hasLevels(b, levels)) return -1; |
||||
else if (!hasLevels(a, levels) && hasLevels(b, levels)) return 1; |
||||
else return 0; |
||||
}) |
||||
|
||||
// Then by patterns |
||||
if (this.pattern) sortFns.push((a, b) => { |
||||
if (a.patterns?.[this.pattern] && !b.patterns?.[this.pattern]) return -1; |
||||
else if (!a.patterns?.[this.pattern] && b.patterns?.[this.pattern]) return 1; |
||||
else return 0; |
||||
}) |
||||
|
||||
// Then sort by tag |
||||
if (this.tag) sortFns.push((a, b) => { |
||||
if (a.tags.map(t => t.name || t).includes(this.tag) && !b.tags.map(t => t.name || t).includes(this.tag)) return -1; |
||||
else if (!a.tags.map(t => t.name || t).includes(this.tag) && b.tags.map(t => t.name || t).includes(this.tag)) return 1; |
||||
else return 0; |
||||
}); |
||||
|
||||
// Then by unit |
||||
if (this.unit) sortFns.push((a, b) => { |
||||
if (a.dataUnits?.map(u => u.title).includes(this.unit.title) && |
||||
!b.dataUnits?.map(u => u.title).includes(this.unit.title)) return -1; |
||||
else if (!a.dataUnits?.map(u => u.title).includes(this.unit.title) && |
||||
b.dataUnits?.map(u => u.title).includes(this.unit.title)) return 1; |
||||
else return 0; |
||||
}); |
||||
|
||||
// Then by time |
||||
sortFns.push((a, b) => new Date(a.meta.timestamp) > new Date(b.meta.timestamp) ? -1 : 1); |
||||
|
||||
clone.sort((a, b) => { |
||||
let value = 1; |
||||
for (let i = 0; i < sortFns.length; i++) { |
||||
value = sortFns[i](a,b); |
||||
if (value !== 0) return value; |
||||
} |
||||
return value; |
||||
}) |
||||
return clone |
||||
}, |
||||
pagedAnnotations () { |
||||
return this.sortedAnnotations.filter((a, i) => i < this.aByPage * this.numPages); |
||||
} |
||||
}, |
||||
mounted () { |
||||
if (this.openWith?.type === 'annotation') { |
||||
this.selectAnnotation(this.openWith.object); |
||||
this.$emit('openedWith'); |
||||
} |
||||
}, |
||||
watch: { |
||||
activeAnnotation () { |
||||
const active = this.activeAnnotation; |
||||
this.activeUnits = []; |
||||
if (active) { |
||||
if (active.replyTo) { |
||||
this.activeParent = this.annotations.filter(a => a.id === (active.replyTo?.id || active.replyTo))[0]; |
||||
} else this.activeParent = null; |
||||
this.answersToActive = this.annotations.filter(a => { console.log(active.id, a.replyTo, active.id === a.replyTo?.id); return active.id === (a.replyTo?.id || a.replyTo)}); |
||||
} else { |
||||
this.activeParent = null; |
||||
this.answersToActive = []; |
||||
} |
||||
}, |
||||
openWith () { |
||||
if (this.openWith?.type === 'annotation') this.selectAnnotation(this.openWith.object) |
||||
this.$emit('openedWith'); |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass" scoped> |
||||
@import '../../assets/styles/generic/_variables' |
||||
|
||||
hr |
||||
border: 1px solid cc("colvis-grey", "dark") |
||||
.col-retrieval--main |
||||
display: flex |
||||
flex-direction: column |
||||
+container("light") |
||||
background: cc("grey", "light") |
||||
min-width: 30rem |
||||
width: 100% |
||||
|
||||
&__header |
||||
+toolbar("light") |
||||
|
||||
&__handle |
||||
position: fixed |
||||
top: 50% |
||||
left: 0 |
||||
transform: translate(-21px, -50%) rotate(90deg) |
||||
background: #fff |
||||
border-radius: 50% 50% 0 0 |
||||
width: 50px |
||||
height: 10px |
||||
box-shadow: $elevation2 |
||||
border: 1px solid rgba(0,0,0,.2) |
||||
cursor: e-resize |
||||
|
||||
.annotations-list |
||||
display: flex |
||||
flex-direction: column |
||||
list-style: none |
||||
padding: 0 |
||||
padding: 2rem |
||||
|
||||
&__item |
||||
margin-top: 1rem |
||||
|
||||
.flip-list-enter-active, .flip-list-leave-active |
||||
transition: opacity .5s |
||||
|
||||
.flip-list-enter, .flip-list-leave-to |
||||
opacity: 0 |
||||
|
||||
.flip-list-move |
||||
transition: transform 1s |
||||
|
||||
.linked |
||||
margin: 0 |
||||
|
||||
> .card |
||||
position: relative |
||||
|
||||
&.parent |
||||
border-left: 5px solid cc("colvis-grey", "light") |
||||
> .card |
||||
transform: scale(.7) |
||||
transform-origin: top left |
||||
|
||||
&.child |
||||
border-right: 5px solid cc("colvis-grey", "light") |
||||
|
||||
&:last-of-type |
||||
margin-bottom: 2rem |
||||
|
||||
> .card |
||||
transform: scale(.7) |
||||
transform-origin: bottom right |
||||
|
||||
.more |
||||
align-self: center |
||||
color: white |
||||
margin-bottom: 1rem |
||||
</style> |
@ -0,0 +1,219 @@
|
||||
<template lang="pug"> |
||||
nav.filter |
||||
ul.tabs |
||||
li(@click="tab = 'units'" :class="{ active: tab === 'units' }") Units |
||||
li(@click="tab = 'tags'" :class="{ active: tab === 'tags' }") Tags |
||||
li(@click="tab = 'patterns'" :class="{ active: tab === 'patterns' }") Patterns |
||||
li(@click="tab = 'loi'" :class="{ active: tab === 'loi' }") Levels of Interpretation |
||||
section.content(v-if="tab") |
||||
small.explanation(v-if="view.explanations" v-for="(e, o) in view.explanations") |
||||
strong {{o}}: |
||||
span {{ e.text }} |
||||
div.content__chips |
||||
c-chip(v-for="item in view" :class="{ selected: selected && selected.name === item.name }" :key="item.name" @click="select(item)") {{ item.name }} |
||||
template(v-if="item.icon" v-slot:prepend) |
||||
c-singularity(v-if="item.name === 'singularity'" color="white") |
||||
c-duality(v-if="item.name === 'duality'" color="white") |
||||
c-generality(v-if="item.name === 'generality'" color="white") |
||||
template(v-slot:append) |
||||
span.count {{ item.annotations.length }} annotations |
||||
</template> |
||||
|
||||
<script> |
||||
import CChip from '@/components/_generic/CChip.vue'; |
||||
import CSingularity from '@/components/_generic/CSingularity.vue'; |
||||
import CDuality from '@/components/_generic/CDuality.vue'; |
||||
import CGenerality from '@/components/_generic/CGenerality.vue'; |
||||
|
||||
export default { |
||||
components: { CChip, CSingularity, CDuality, CGenerality }, |
||||
props: { |
||||
annotations: { type: Array, default: () => [] }, |
||||
openWith: { type: Object } |
||||
}, |
||||
data () { |
||||
return { |
||||
tab: null, |
||||
selected: null |
||||
} |
||||
}, |
||||
methods: { |
||||
select (item) { |
||||
this.selected = item.name === this.selected?.name ? null : item; |
||||
this.$emit('select', this.selected); |
||||
} |
||||
}, |
||||
computed: { |
||||
tags () { |
||||
const tags = this.annotations.reduce( |
||||
(m, annotation) => { |
||||
const tags = annotation.tags.map(t => t.name || t); |
||||
tags.forEach(t => { |
||||
if (!m.has(t)) m.set(t, []); |
||||
m.get(t).push(annotation); |
||||
}); |
||||
return m; |
||||
}, |
||||
new Map() |
||||
); |
||||
return Array.from(tags).map(t => ({ name: t[0], annotations: t[1] })) |
||||
.sort((a, b) => a.annotations.length > b.annotations.length ? -1 : 1); |
||||
}, |
||||
units () { |
||||
const units = this.annotations.reduce( |
||||
(m, annotation) => { |
||||
const units = annotation.dataUnits; |
||||
if (!units) return m; |
||||
|
||||
units.forEach(u => { |
||||
if (!m.has(u.title)) m.set(u.title, []) |
||||
m.get(u.title).push(annotation) |
||||
}); |
||||
return m; |
||||
}, |
||||
new Map() |
||||
); |
||||
return Array.from(units).map(u => ({ name: u[0], annotations: u[1] })) |
||||
.sort((a, b) => a.annotations.length > b.annotations.length ? -1 : 1); |
||||
}, |
||||
patterns () { |
||||
const patterns = new Map(); |
||||
patterns.set('singularity', []); |
||||
patterns.set('duality', []); |
||||
patterns.set('generality', []); |
||||
this.annotations.forEach(annotation => { |
||||
if (annotation.patterns?.singularity) patterns.get('singularity').push(annotation) |
||||
if (annotation.patterns?.duality) patterns.get('duality').push(annotation) |
||||
if (annotation.patterns?.generality) patterns.get('generality').push(annotation) |
||||
}); |
||||
|
||||
const explanations = { |
||||
singularity: { text: 'Denotes a comparison between a small group of entites and a larger one.' }, |
||||
duality: { text: 'Denotes a comparison between two or more groups of similar size.' }, |
||||
generality: { text: 'Denotes a general comment about the whole dataset of entites.' } |
||||
} |
||||
|
||||
const patternsArray = Array.from(patterns).map(p => ({ name: p[0], annotations: p[1], icon: true })) |
||||
.sort((a, b) => a.annotations.length > b.annotations.length ? -1 : 1); |
||||
|
||||
patternsArray.explanations = explanations; |
||||
|
||||
return patternsArray; |
||||
}, |
||||
loi () { |
||||
const loi = new Map(); |
||||
loi.set('visual', []); |
||||
loi.set('data', []); |
||||
loi.set('meaning', []); |
||||
loi.set('visual and data', []); |
||||
loi.set('visual and meaning', []); |
||||
loi.set('data and meaning', []); |
||||
loi.set('all', []); |
||||
this.annotations.forEach(annotation => { |
||||
const l = annotation.loi; |
||||
if (!l) return; |
||||
if (l.visual && !l.data && !l.meaning) loi.get('visual').push(annotation); |
||||
else if (!l.visual && l.data && !l.meaning) loi.get('data').push(annotation); |
||||
else if (!l.visual && !l.data && l.meaning) loi.get('meaning').push(annotation); |
||||
else if (l.visual && l.data && !l.meaning) loi.get('visual and data').push(annotation); |
||||
else if (l.visual && !l.data && l.meaning) loi.get('visual and meaning').push(annotation); |
||||
else if (l.visual && !l.data && l.meaning) loi.get('data and meaning').push(annotation); |
||||
else if (l.visual && l.data && l.meaning) loi.get('all').push(annotation); |
||||
}); |
||||
|
||||
const explanations = { |
||||
visual: { text: 'The annotator relied on visual cues to formulate the annotation.' }, |
||||
data: { text: 'The annotator relied on prior knowledge of the dataset to formulate the annotation.' }, |
||||
meaning: { text: 'The annotator formulated an hypothesis based on the data selected.' } |
||||
} |
||||
|
||||
const loiArray = Array.from(loi).map(l => ({ name: l[0], annotations: l[1], explanation: explanations[l[0]] })) |
||||
.sort((a, b) => a.annotations.length > b.annotations.length ? -1 : 1); |
||||
|
||||
loiArray.explanations = explanations; |
||||
|
||||
return loiArray; |
||||
}, |
||||
view () { |
||||
if (!this.tab) return null; |
||||
else return this[this.tab]; |
||||
} |
||||
}, |
||||
mounted () { |
||||
if (this.openWith?.type === 'filter') { |
||||
const { tab, selected } = this.openWith.object; |
||||
this.tab = tab; |
||||
this.select(this[tab].filter(s => s.name === selected)[0]); |
||||
} |
||||
this.$emit('openedWith'); |
||||
}, |
||||
watch: { |
||||
openWith () { |
||||
if (this.openWith?.type === 'filter') { |
||||
const { tab, selected } = this.openWith.object; |
||||
this.tab = tab; |
||||
this.select(this[tab].filter(s => s.name === selected)[0]); |
||||
} |
||||
this.$emit('openedWith'); |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass" scoped> |
||||
@import '../../assets/styles/generic/_variables' |
||||
|
||||
.filter |
||||
background: cc("colvis-grey") |
||||
color: white |
||||
box-shadow: $elevation |
||||
|
||||
.tabs |
||||
list-style: none |
||||
display: flex |
||||
max-width: 30rem |
||||
padding: 0 |
||||
margin: 0 auto |
||||
|
||||
> li |
||||
flex: 1 |
||||
text-align: center |
||||
cursor: pointer |
||||
padding: .25rem |
||||
|
||||
&:hover, &.active |
||||
background: cc("colvis-grey", "dark") |
||||
|
||||
.content |
||||
max-width: 30rem |
||||
margin: 1rem auto 0 auto |
||||
|
||||
&__chips |
||||
height: 10rem |
||||
overflow-y: auto |
||||
display: flex |
||||
flex-wrap: wrap |
||||
align-items: stretch |
||||
justify-content: stretch |
||||
|
||||
> * |
||||
transition: all .2s ease-in-out |
||||
flex: 1 |
||||
display: flex |
||||
align-items: center |
||||
justify-content: space-around |
||||
|
||||
&.selected |
||||
flex: 1 100% |
||||
order: -1 |
||||
background: cc("blue", "dark") |
||||
|
||||
.explanation |
||||
display: block |
||||
|
||||
.count |
||||
background: cc("grey", "dark") |
||||
border-radius: 5px |
||||
padding: .25rem |
||||
display: inline-block |
||||
</style> |
Loading…
Reference in new issue