Browse Source

Main retrieval view

master
Pierre Vanhulst 2 years ago
parent
commit
f60ffaf12f
  1. 831
      dist/colvis-client.common.js
  2. 2
      dist/colvis-client.common.js.map
  3. 2
      dist/colvis-client.css
  4. 831
      dist/colvis-client.umd.js
  5. 2
      dist/colvis-client.umd.js.map
  6. 2
      dist/colvis-client.umd.min.js
  7. 2
      dist/colvis-client.umd.min.js.map
  8. 4
      example/src/App.vue
  9. 48
      example/src/assets/prior-annotations.json
  10. 2
      src/components/Input/Box.vue
  11. 8
      src/components/Retrieval/CCard.vue
  12. 1
      src/components/Retrieval/List.vue
  13. 294
      src/components/Retrieval/Main.vue
  14. 219
      src/components/_retrieval/CFilter.vue
  15. 8
      src/components/_retrieval/CToolbar.vue
  16. 2
      src/lib.js

831
dist/colvis-client.common.js vendored

File diff suppressed because one or more lines are too long

2
dist/colvis-client.common.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/colvis-client.css vendored

File diff suppressed because one or more lines are too long

831
dist/colvis-client.umd.js vendored

File diff suppressed because one or more lines are too long

2
dist/colvis-client.umd.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/colvis-client.umd.min.js vendored

File diff suppressed because one or more lines are too long

2
dist/colvis-client.umd.min.js.map vendored

File diff suppressed because one or more lines are too long

4
example/src/App.vue

@ -33,6 +33,10 @@
<v-switch v-model="dummyState2" label="Dummy state 2"/>
<v-switch v-model="dummyState3" label="Dummy state 3"/>
</v-flex>
<v-flex md6>
<ColRetrievalMain :annotations="annotations" :replyTo="replyTo" @endorse="endorse" @funny="funny" @avatar="doSomething">
</ColRetrievalMain>
</v-flex>
</v-layout>
</v-content>
</v-app>

48
example/src/assets/prior-annotations.json

@ -12488,6 +12488,18 @@
{
"id": 6,
"replyTo": 7,
"social": {
"author": {
"id": 1,
"name": "Albert",
"avatar": "http://127.0.0.1:1337/uploads/small_Ernst_avatar_7ad69ee5c1.png"
},
"funny": true,
"stats": {
"endorsed": 0,
"funny": 1
}
},
"dataUnits": [
{
"role": "subject",
@ -13023,6 +13035,18 @@
],
"text": "rightmost cars stand out.",
"state": "__vue_devtool_undefined__",
"social": {
"author": {
"id": 1,
"name": "Albert",
"avatar": "http://127.0.0.1:1337/uploads/small_Ernst_avatar_7ad69ee5c1.png"
},
"funny": true,
"stats": {
"endorsed": 0,
"funny": 1
}
},
"replyTo": {
"id": 7,
"dataUnits": [
@ -13575,6 +13599,18 @@
},
"tags": [],
"replyTo": 7,
"social": {
"author": {
"id": 1,
"name": "Albert",
"avatar": "http://127.0.0.1:1337/uploads/small_Ernst_avatar_7ad69ee5c1.png"
},
"funny": true,
"stats": {
"endorsed": 0,
"funny": 1
}
},
"meta": {
"timestamp": 1603812389136,
"hashes": {
@ -13596,6 +13632,18 @@
"test"
],
"replyType": "comment",
"social": {
"author": {
"id": 1,
"name": "Albert",
"avatar": "http://127.0.0.1:1337/uploads/small_Ernst_avatar_7ad69ee5c1.png"
},
"funny": true,
"stats": {
"endorsed": 0,
"funny": 1
}
},
"replyTo": {
"id": 1,
"dataUnits": [

2
src/components/Input/Box.vue

@ -143,7 +143,7 @@ export default {
meaning: conclusion,
specs: this.$colvis.getSpecs(),
ci: this.$colvis,
reason, selectionMethods, tags, subjectName, complementName, replyTo: replyTo.id, replyType
reason, selectionMethods, tags, subjectName, complementName, replyTo: replyTo?.id, replyType
});
},
/** @returns {Annotation|Boolean} the annotation */

8
src/components/Retrieval/CCard.vue

@ -35,6 +35,10 @@
reply-icon.reply(@click="$emit('reply', annotation)" :class="{ selected: replying }" :color="null" :size="48")
thumb-icon(@click="$emit('endorse', annotation)" :class="{ selected: annotation.social.endorsed }" :color="null" :size="48")
funny-icon(@click="$emit('funny', annotation)" :class="{ selected: annotation.social.funny }" :color="null" :size="48")
div.stats
small(v-if="annotation.replyTo") this annotation is a reply of type "{{ annotation.replyType }}"
small(v-if="replies.length") this annotation has {{ replies.length }} replies
slot(name="extension" :annotation="annotation")
</template>
@ -58,7 +62,8 @@ export default {
activeAnnotation: { type: Object },
maxWidth: { type: Boolean, default: false },
filter: { type: Boolean, default: false },
replyTo: { type: Object }
replyTo: { type: Object },
replies: { type: Array, default: () => [] }
},
methods: { strfy },
computed: {
@ -124,6 +129,7 @@ export default {
fill: cc("colvis-grey")
display: flex
margin-bottom: .5rem
justify-content: space-between
> .stat
display: flex

1
src/components/Retrieval/List.vue

@ -8,6 +8,7 @@
:unit="unit" :tag="tag" :pattern="pattern" :annotations="annotations"
:loi="loi" :activeAnnotation="activeAnnotation"
@removeSort="removeSort" @search="needle = $event"
state
)
button.col-retrieval--viz__handle(@mousedown="start" @mousemove="move" @mouseup="stop")
transition-group(ref="list" name="flip-list" tag="ul" class="annotations-list")

294
src/components/Retrieval/Main.vue

@ -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>

219
src/components/_retrieval/CFilter.vue

@ -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>

8
src/components/_retrieval/CToolbar.vue

@ -7,7 +7,7 @@
div.toolbar__sort
span Sort by:
c-chip(inactive) Time
c-chip(inactive) Current state
c-chip(v-if="state" inactive) Current state
c-chip(v-if="unit" @close="$emit('removeSort', 'unit')" closeable) Unit: {{ unit.title }}
c-chip(v-if="tag" @close="$emit('removeSort', 'tag')" closeable) Tag: {{ tag }}
c-chip(v-if="pattern" @close="$emit('removeSort', 'pattern')" closeable) Pattern: {{ pattern }}
@ -26,7 +26,8 @@ export default {
tag: { type: String },
pattern: { type: String },
loi: { type: String },
activeAnnotation: { type: Object }
activeAnnotation: { type: Object },
state: { type: Boolean, default: false }
},
data () {
return { needle: null }
@ -56,9 +57,10 @@ export default {
> label
width: 100%
display: inline-block
display: flex
&__input
flex: 1
color: white
&__sort

2
src/lib.js

@ -1,5 +1,6 @@
import ColInputMain from './components/Input/Main.vue';
import ColRetrievalViz from './components/Retrieval/Viz.vue';
import ColRetrievalMain from './components/Retrieval/Main.vue';
import CText from './components/Retrieval/CText.vue';
import CCard from './components/Retrieval/CCard.vue';
import { getDataFromContainer, hashCode, strfy } from './assets/utils';
@ -14,6 +15,7 @@ const ColvisPlugin = {
install(Vue) {
/** Registering the main components */
Vue.component('ColInputMain', ColInputMain);
Vue.component('ColRetrievalMain', ColRetrievalMain);
Vue.component('ColRetrievalViz', ColRetrievalViz);
Vue.component('CText', CText);
Vue.component('CCard', CCard);

Loading…
Cancel
Save