Browse Source
- Added Babel for ES2020 - Added Retrieval Viz to example - Worked on Retrieval componentsmaster
31 changed files with 14886 additions and 629 deletions
@ -1,5 +1,4 @@
|
||||
module.exports = { |
||||
presets: [ |
||||
'@vue/app' |
||||
] |
||||
presets: [ '@vue/app' ], |
||||
plugins: [ '@babel/plugin-proposal-optional-chaining', '@babel/plugin-syntax-nullish-coalescing-operator' ] |
||||
} |
||||
|
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
File diff suppressed because one or more lines are too long
@ -1,7 +1,36 @@
|
||||
@import './_variables' |
||||
|
||||
@keyframes col-rotate |
||||
0% |
||||
stroke-dasharray: 5 |
||||
stroke-width: 2 |
||||
50% |
||||
stroke-dasharray: 15 |
||||
stroke-width: 6 |
||||
100% |
||||
stroke-dasharray: 5 |
||||
stroke-width: 2 |
||||
|
||||
@keyframes col-giggle |
||||
0% |
||||
box-shadow: $elevation |
||||
transform: translateX(-.5rem) |
||||
50% |
||||
box-shadow: $elevation2 |
||||
transform: translateX(.5rem) |
||||
100% |
||||
box-shadow: $elevation |
||||
transform: translateX(-.5rem) |
||||
|
||||
@keyframes col-loading |
||||
0% |
||||
width: 10% |
||||
left: 0 |
||||
|
||||
20% |
||||
width: 70% |
||||
left: 20% |
||||
|
||||
100% |
||||
width: 10% |
||||
left: 100% |
||||
|
@ -0,0 +1,103 @@
|
||||
<template lang="pug"> |
||||
div.text |
||||
span.text__unit(@click.stop="subjectVisible = !subjectVisible") |
||||
c-chip(:text="subject.title" :inactive="!subject.units.length" @mouseover="$emit('highlight', subject)" @mouseout="$emit('highlight')") |
||||
div.text__unit__ag(v-if="subject.units.length" :class="{ active: subjectVisible }") |
||||
c-chip(v-for="unit in subject.units" :text="unit.title" inactive @mouseover="$emit('highlight', unit)" @mouseout="$emit('highlight')") |
||||
span.text__verb {{ verb }} |
||||
span.text__unit(v-if="complement" @click.stop="complementVisible = !complementVisible") |
||||
c-chip(:text="complement.title" :inactive="!complement.units.length" @mouseover="$emit('highlight', complement)" @mouseout="$emit('highlight')") |
||||
div.text__unit__ag(v-if="complement.units.length" :class="{ active: complementVisible }") |
||||
c-chip(v-for="unit in complement.units" :text="unit.title" inactive @mouseover="$emit('highlight', unit)" @mouseout="$emit('highlight')") |
||||
span.text__details(v-if="details") {{ details }} |
||||
span.text__meaning(v-if="meaning") {{ meaning }} |
||||
</template> |
||||
|
||||
<script> |
||||
import CChip from '@/components/_generic/CChip.vue'; |
||||
|
||||
export default { |
||||
components: { CChip }, |
||||
props: { |
||||
annotation: { type: Object, required: true } |
||||
}, |
||||
data () { |
||||
return { |
||||
subjectVisible: false, |
||||
complementVisible: false |
||||
} |
||||
}, |
||||
computed: { |
||||
subject () { |
||||
return this.annotation.dataUnits.filter(u => u.role === 'subject')[0]; |
||||
}, |
||||
verb () { |
||||
const n = this.subject.units.length > 1 ? 'plural' : 'singular'; |
||||
let verb = this.annotation.rawAnnotation.reason.verb[n]; |
||||
if (this.complement) verb += ` ${this.annotation.rawAnnotation.reason.verb.comparison}`; |
||||
else verb += `.`; |
||||
return ` ${verb}`; |
||||
}, |
||||
complement () { |
||||
return this.annotation.dataUnits.filter(u => u.role === 'complement')[0]; |
||||
}, |
||||
details () { |
||||
let d = this.annotation.rawAnnotation.reason.details; |
||||
if (!d) return; |
||||
d = d.substr(-1).match(/^[.,:!? ]/) ? d : `${d}.`; |
||||
return ` ${d}`; |
||||
}, |
||||
meaning () { |
||||
let m = this.annotation.rawAnnotation.meaning; |
||||
if (!m) return; |
||||
m = m.substr(-1).match(/^[.,:!? ]/) ? m : `${m}.`; |
||||
return ` ${m}`; |
||||
} |
||||
}, |
||||
watch: { |
||||
subjectVisible () { |
||||
if (this.subjectVisible) this.complementVisible = false; |
||||
}, |
||||
complementVisible () { |
||||
if (this.complementVisible) this.subjectVisible = false; |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass" scoped> |
||||
@import '../../assets/styles/generic/_variables' |
||||
|
||||
.text |
||||
padding: .5rem .5rem .5rem 2rem |
||||
position: relative |
||||
|
||||
&::before |
||||
position: absolute |
||||
content: "”" |
||||
font-size: 4em |
||||
opacity: .5 |
||||
font-family: serif |
||||
top: 0 |
||||
left: 0 |
||||
width: 100% |
||||
height: 100% |
||||
|
||||
&__unit |
||||
position: relative |
||||
&__ag |
||||
position: absolute |
||||
left: 50% |
||||
bottom: 0 |
||||
transform: translate(-50%, 100%) |
||||
display: none |
||||
width: 20em |
||||
background: rgba(255, 255, 255, .4) |
||||
backdrop-filter: blur(3px) |
||||
padding: .5rem |
||||
border-radius: 3px |
||||
box-shadow: $elevation |
||||
z-index: 150 |
||||
&.active |
||||
display: initial |
||||
</style> |
@ -0,0 +1,103 @@
|
||||
<template lang="pug"> |
||||
aside.col-retrieval--viz |
||||
transition-group(name="flip-list" tag="ul" class="annotations-list") |
||||
li.annotations-list__item(v-for="annotation in sortedAnnotations" :key="annotation.meta.timestamp") |
||||
c-card(:annotation="annotation" :activeAnnotation="activeAnnotation" @highlight="$emit('highlight', $event)" @selectAnnotation="$emit('selectAnnotation', annotation)") |
||||
template(v-slot:extension) |
||||
slot(name="card") |
||||
</template> |
||||
|
||||
<script> |
||||
import CCard from '@/components/Retrieval/CCard.vue'; |
||||
import CLogo from '../_generic/CLogo.vue'; |
||||
|
||||
export default { |
||||
components: { CCard, CLogo }, |
||||
props: { |
||||
/** |
||||
* The current state of the application's visualization. |
||||
* Used to display relevant annotations |
||||
*/ |
||||
state: { |
||||
type: Object, |
||||
default: () => {} |
||||
}, |
||||
/** |
||||
* The list of prior annotations |
||||
*/ |
||||
annotations: { |
||||
type: Array, |
||||
default: () => [] |
||||
}, |
||||
/** |
||||
* Whether the list should be visible |
||||
*/ |
||||
viewing: { |
||||
type: Boolean, |
||||
default: false |
||||
}, |
||||
activeAnnotation: { |
||||
type: Object |
||||
} |
||||
}, |
||||
data () { |
||||
return { open: false } |
||||
}, |
||||
computed: { |
||||
selectedSubjects () { |
||||
if (!this.activeAnnotation) return []; |
||||
else { |
||||
return this.$colvis.getAnnotationDataBinders(this.activeAnnotation); |
||||
} |
||||
}, |
||||
sortedAnnotations () { |
||||
const clone = JSON.parse(JSON.stringify(this.annotations)); |
||||
const state = this.$colvis.getVizState(); |
||||
clone.forEach(a => { |
||||
const s = a.meta.state; |
||||
let sim = 0; |
||||
for(const prop in s) { |
||||
console.log(prop, s[prop], state[prop]); |
||||
if (s[prop] === state[prop]) sim += 1; |
||||
} |
||||
a.sim = sim; |
||||
}) |
||||
clone.sort((a, b) => new Date(a.meta.timestamp > new Date(b.meta.timestamp) ? 1 : -1)); |
||||
clone.sort((a, b) => a.sim > b.sim ? -1 : 1); |
||||
return clone |
||||
} |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass" scoped> |
||||
@import '../../assets/styles/generic/_variables' |
||||
|
||||
.col-retrieval--viz |
||||
position: fixed |
||||
right: 0 |
||||
top: 0 |
||||
height: 100% |
||||
min-width: 25rem |
||||
transition: transform .2s ease-in-out |
||||
background: rgba(255, 255, 255, .3) |
||||
backdrop-filter: blur(2px) |
||||
z-index: 100 |
||||
overflow-x: hidden |
||||
overflow-y: auto |
||||
+container("light") |
||||
display: flex |
||||
justify-content: center |
||||
|
||||
.annotations-list |
||||
display: flex |
||||
flex-direction: column |
||||
list-style: none |
||||
padding: 0 |
||||
|
||||
&__item |
||||
margin-top: 1rem |
||||
|
||||
.flip-list-move |
||||
transition: transform 1s |
||||
</style> |
@ -0,0 +1,209 @@
|
||||
<template lang="pug"> |
||||
svg.col-retrieval-viewer( |
||||
v-if="$colvis.hasViewer()" |
||||
:class="{ active: activeUnits.length }" |
||||
ref="selection" |
||||
:style="{ top: `${compTop}px`, left: `${compLeft}px`, width: `${width}px`, height: `${height}px` }" |
||||
) |
||||
path.col-retrieval-viewer__selected( |
||||
v-for="(el, i) in pathPool" |
||||
:key="i" |
||||
:class="{ subject: el.role === 'subject', complement: el.role === 'complement' }" |
||||
:transform="getTransform(el)" |
||||
:d="el.domElement.attributes.d.value" |
||||
) |
||||
circle.col-retrieval-viewer__selected( |
||||
v-for="(el, i) in circlePool" |
||||
:key="i" |
||||
:class="{ subject: el.role === 'subject', complement: el.role === 'complement' }" |
||||
:transform="getTransform(el)" |
||||
:cx="el.domElement.cx.animVal.value" |
||||
:cy="el.domElement.cy.animVal.value" |
||||
:r="el.domElement.r.animVal.value" |
||||
) |
||||
rect.col-retrieval-viewer__selected( |
||||
v-for="(el, i) in rectPool" |
||||
:key="i" |
||||
:class="{ subject: el.role === 'subject', complement: el.role === 'complement' }" |
||||
:transform="getTransform(el)" |
||||
:x="el.domElement.attributes.x.value" |
||||
:y="el.domElement.attributes.y.value" |
||||
:width="el.domElement.attributes.width.value" |
||||
:height="el.domElement.attributes.height.value" |
||||
) |
||||
</template> |
||||
|
||||
<script> |
||||
|
||||
/** |
||||
* Create a new Selector. |
||||
* Selectors are the visual interface that allows analysts to select data units from their visualizations. |
||||
* @extends Vue |
||||
*/ |
||||
export default { |
||||
props: { |
||||
/** Whether the visualization has been initialized */ |
||||
init: { type: Boolean, required: true }, |
||||
/** The list of selected units */ |
||||
activeUnits: { type: Array, default: () => [] }, |
||||
}, |
||||
data () { |
||||
/** The top position of the Selector, in pixels */ |
||||
const top = 0; |
||||
/** The left position of the Selector, in pixels */ |
||||
const left = 0; |
||||
/** The width of the Selector, in pixels */ |
||||
const width = 0; |
||||
/** The height of the Selector, in pixels */ |
||||
const height = 0; |
||||
/** Some parent elements of the Selector might be in Position relative */ |
||||
const offsetTop = 0; |
||||
const offsetLeft = 0; |
||||
/** List of event listeners to remove once the component is destroyed */ |
||||
const eventListeners = [] |
||||
/** ResizeObserver to make sure that the selector matches the SVG it overlays */ |
||||
const resizeObserver = null; |
||||
|
||||
return { top, left, width, height, offsetTop, offsetLeft, eventListeners, resizeObserver }; |
||||
}, |
||||
computed: { |
||||
/** @returns the list of selected DataBinders, either as subjects or complements. */ |
||||
pool() { |
||||
return this.$colvis.getDataBinders(this.activeUnits); |
||||
}, |
||||
|
||||
/** @returns the list of paths within the selected DataBinders. */ |
||||
pathPool() { |
||||
return this.pool.filter((binder) => binder.domElement.tagName === 'path'); |
||||
}, |
||||
|
||||
/** @returns the list of circles within the selected DataBinders. */ |
||||
circlePool() { |
||||
return this.pool.filter((binder) => binder.domElement.tagName === 'circle'); |
||||
}, |
||||
|
||||
/** @returns the list of rectangles within the selected DataBinders. */ |
||||
rectPool() { |
||||
return this.pool.filter((binder) => binder.domElement.tagName === 'rect'); |
||||
}, |
||||
|
||||
compLeft() { |
||||
return this.left - this.offsetLeft; |
||||
}, |
||||
|
||||
compTop() { |
||||
return this.top - this.offsetTop; |
||||
} |
||||
}, |
||||
methods: { |
||||
/** |
||||
* Return the translation value of a given element, based on its transformation matrix. |
||||
* It allows to restore the element's precise position despite potential transformations |
||||
* implied by parents. |
||||
* @param {DataBinder} element the element whose translate value is required |
||||
*/ |
||||
getTransform(element) { |
||||
if (!(element.domElement instanceof SVGGraphicsElement)) |
||||
throw new Error('The selected element is not a SVG element'); |
||||
const matrix = element.domElement.getCTM(); |
||||
return matrix ? `translate(${matrix.e}, ${matrix.f})` : ``; |
||||
}, |
||||
|
||||
/** |
||||
* Get the position of the container of the visualization. |
||||
* Setting { top, left, width, height } properties of the instance. |
||||
*/ |
||||
getPosition() { |
||||
function position () { |
||||
const target = document.querySelector(this.$colvis.getSpecs().visualization.container); |
||||
if (!target) { |
||||
throw new Error('Could not locate a target container for the Selector. Please make sure the specifications is set correctly.'); |
||||
} |
||||
|
||||
let offsetTop = 0; |
||||
let offsetLeft = 0; |
||||
let offsetter = this.$refs.selection.parentElement; |
||||
while (offsetter.offsetParent) { |
||||
offsetTop += offsetter.offsetParent.offsetTop; |
||||
offsetLeft += offsetter.offsetParent.offsetLeft; |
||||
offsetter = offsetter.offsetParent; |
||||
} |
||||
const { top, left, width, height } = target.getBoundingClientRect(); |
||||
Object.assign(this.$data, { top, left, width, height, offsetTop, offsetLeft }); |
||||
} |
||||
|
||||
position.call(this); |
||||
|
||||
if (this.height <= 0) { |
||||
console.warn('Could not attach Selector. Will try again in 1 second.'); |
||||
const interval = window.setInterval(e => { |
||||
position.call(this); |
||||
if (this.height > 0) { |
||||
console.log('Selector attached'); |
||||
clearInterval(interval); |
||||
} else console.warn('Could not attach Selector. Will try again in 1 second.'); |
||||
}, 1000) |
||||
} |
||||
}, |
||||
}, |
||||
watch: { |
||||
init () { |
||||
if (this.$colvis.isInit()) this.getPosition(); |
||||
} |
||||
}, |
||||
mounted () { |
||||
this.getPosition(); |
||||
|
||||
addEventListener('resize', this.getPosition); |
||||
this.eventListeners.push({ resize: this.getPosition }); |
||||
/** |
||||
* We resize the selector when the selector's parent's element is resized. We cannot observe |
||||
* SVGs, since there are multiple issues with SVG and bounding boxes. |
||||
*/ |
||||
this.resizeObserver = new ResizeObserver(e => { console.log('triggering resize'); this.getPosition(); }); |
||||
this.resizeObserver.observe(this.$refs.selection.parentElement); |
||||
}, |
||||
destroyed () { |
||||
this.eventListeners.forEach(e => { |
||||
Object.keys(e).forEach(key => { |
||||
removeEventListener(key, e[key]); |
||||
}); |
||||
}); |
||||
this.resizeObserver.disconnect(); |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass"> |
||||
@import "../../assets/styles/generic/_animations" |
||||
@import "../../assets/styles/generic/_variables" |
||||
|
||||
.col-retrieval-viewer |
||||
position: absolute |
||||
pointer-events: none |
||||
z-index: 4 |
||||
|
||||
&.active |
||||
background-color: rgba(sc("red"), .2) |
||||
pointer-events: auto |
||||
|
||||
.col-input-selector__selection, .col-input-selector__selected |
||||
opacity: 1 |
||||
|
||||
&__selection, &__selected |
||||
opacity: .9 |
||||
fill: rgba(100, 100, 100, .3) |
||||
stroke: rgb(100, 100, 100) |
||||
mix-blend-mode: multiply |
||||
|
||||
&.subject |
||||
fill: rgba(sc("blue"), .3) |
||||
stroke: sc("blue") |
||||
stroke-dasharray: 5 |
||||
animation: col-rotate 2s infinite |
||||
&.complement |
||||
fill: rgba(sc("yellow"), .3) |
||||
stroke: sc("yellow") |
||||
stroke-dasharray: 5 |
||||
animation: col-rotate 2s infinite |
||||
</style> |
@ -1,19 +0,0 @@
|
||||
<template lang="pug"> |
||||
div.annotation {{ annotation.rawAnnotation.meaning }} |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
props: { |
||||
annotation: { |
||||
type: Object, |
||||
required: true |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="sass" scoped> |
||||
.annotation |
||||
width: 200px |
||||
</style> |
Loading…
Reference in new issue