Merge branch '306_visgraph_corrections' into 'master'

306 fix visgraph issues

See merge request Chill-Projet/chill-bundles!245
This commit is contained in:
Julien Fastré 2021-12-06 14:01:51 +00:00
commit d2c61a26ea
8 changed files with 114 additions and 76 deletions

View File

@ -13,10 +13,11 @@ and this project adheres to
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) * [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
* [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) * [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
* [person search] fix bug when using birthdate after and birthdate before * [person search] fix bug when using birthdate after and birthdate before
* [person search] increase pertinence when lastname begins with search pattern * [person search] increase pertinence when lastname begins with search pattern
* [activity] create work if a work with same social action is not associated to the activity * [activity] create work if a work with same social action is not associated to the activity
* [visgraph] improve and fix bugs on vis-network relationship graph
* [bugfix] posting of birth- and deathdate through api fixed.
## Test releases ## Test releases
@ -27,6 +28,7 @@ and this project adheres to
* [activity] layout for issues / actions * [activity] layout for issues / actions
* [activity][bugfix] in edit mode, the form will now load the social action list * [activity][bugfix] in edit mode, the form will now load the social action list
### Test release 2021-11-29 ### Test release 2021-11-29
* [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119) * [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119)
@ -53,7 +55,9 @@ and this project adheres to
* add an endpoint for checking permissions. See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/232 * add an endpoint for checking permissions. See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/232
* [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties * [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties * [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [bugfix] posting of birth- and deathdate through api fixed.
## Test releases
### Test release 2021-11-22 ### Test release 2021-11-22

View File

@ -53,6 +53,7 @@
<div class="row"> <div class="row">
<div class="col-12 text-center">{{ $t('visgraph.between') }}<br>{{ $t('visgraph.and') }}</div> <div class="col-12 text-center">{{ $t('visgraph.between') }}<br>{{ $t('visgraph.and') }}</div>
<div class="col"> <div class="col">
<small>{{ getPersonAge(modal.data.from) }}</small>
<h4>{{ getPerson(modal.data.from).text }}</h4> <h4>{{ getPerson(modal.data.from).text }}</h4>
<p class="text-start" v-if="relation && relation.title"> <p class="text-start" v-if="relation && relation.title">
<span v-if="reverse"> <span v-if="reverse">
@ -64,6 +65,7 @@
</p> </p>
</div> </div>
<div class="col text-end"> <div class="col text-end">
<small>{{ getPersonAge(modal.data.to) }}</small>
<h4>{{ getPerson(modal.data.to).text }}</h4> <h4>{{ getPerson(modal.data.to).text }}</h4>
<p class="text-end" v-if="relation && relation.title"> <p class="text-end" v-if="relation && relation.title">
<span v-if="reverse"> <span v-if="reverse">
@ -119,8 +121,9 @@ import vis from 'vis-network/dist/vis-network'
import { mapState, mapGetters } from "vuex" import { mapState, mapGetters } from "vuex"
import Modal from 'ChillMainAssets/vuejs/_components/Modal' import Modal from 'ChillMainAssets/vuejs/_components/Modal'
import VueMultiselect from 'vue-multiselect' import VueMultiselect from 'vue-multiselect'
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api"; import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api"
import { splitId } from "./vis-network"; import { splitId, getAge } from "./vis-network"
import { visMessages } from "./i18n";
export default { export default {
name: "App", name: "App",
@ -128,6 +131,7 @@ export default {
Modal, Modal,
VueMultiselect VueMultiselect
}, },
props: ['household_id'],
data() { data() {
return { return {
container: '', container: '',
@ -152,7 +156,9 @@ export default {
class: null, class: null,
text: null text: null
}, },
} },
canvas: null,
link: null,
} }
}, },
computed: { computed: {
@ -164,7 +170,7 @@ export default {
]), ]),
visgraph_data() { visgraph_data() {
console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges') //console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges')
return { return {
nodes: this.nodes, nodes: this.nodes,
edges: this.edges edges: this.edges
@ -172,12 +178,12 @@ export default {
}, },
refreshNetwork() { refreshNetwork() {
console.log('--- refresh network') //console.log('--- refresh network')
window.network.setData(this.visgraph_data) window.network.setData(this.visgraph_data)
}, },
legendLayers() { legendLayers() {
console.log('--- refresh legend and rebuild checked Layers') //console.log('--- refresh legend and rebuild checked Layers')
this.checkedLayers = [] this.checkedLayers = []
let layersDisplayed = [ let layersDisplayed = [
...this.nodes.filter(n => n.id.startsWith('household')), ...this.nodes.filter(n => n.id.startsWith('household')),
@ -193,7 +199,7 @@ export default {
}, },
checkedLayers() { // required to refresh data checkedLayers checkedLayers() { // required to refresh data checkedLayers
console.log('--- checkedLayers') //console.log('--- checkedLayers')
return this.checkedLayers return this.checkedLayers
}, },
@ -218,7 +224,7 @@ export default {
}, },
watch: { watch: {
updateHack(newValue, oldValue) { updateHack(newValue, oldValue) {
console.log(`--- updateHack ${oldValue} <> ${newValue}`) //console.log(`--- updateHack ${oldValue} <> ${newValue}`)
if (oldValue !== newValue) { if (oldValue !== newValue) {
this.forceUpdateComponent() this.forceUpdateComponent()
} }
@ -229,6 +235,9 @@ export default {
this.initGraph() this.initGraph()
this.listenOnGraph() this.listenOnGraph()
this.getRelationsList() this.getRelationsList()
this.canvas = document.getElementById('visgraph').querySelector('canvas')
this.link = document.getElementById('exportCanvasBtn')
}, },
methods: { methods: {
@ -255,27 +264,27 @@ export default {
case 'person': case 'person':
let person = this.nodes.filter(n => n.id === node)[0] let person = this.nodes.filter(n => n.id === node)[0]
console.log('@@@@@@ event on selected Node', person.id) //console.log('@@@@@@ event on selected Node', person.id)
if (this.listenPersonFlag === 'normal') { if (this.listenPersonFlag === 'normal') {
if (person.folded === true) { if (person.folded === true) {
console.log(' @@> expand mode event') //console.log(' @@> expand mode event')
this.$store.commit('unfoldPerson', person) this.$store.commit('unfoldPerson', person)
} }
} else { } else {
console.log(' @@> create link mode event') //console.log(' @@> create link mode event')
this.listenStepsToAddRelationship(person) this.listenStepsToAddRelationship(person)
} }
break break
case 'household': case 'household':
let household = this.nodes.filter(n => n.id === node)[0] let household = this.nodes.filter(n => n.id === node)[0]
console.log('@@@@@@ event on selected Node', household.id) //console.log('@@@@@@ event on selected Node', household.id)
this.$store.dispatch('unfoldPersonsByHousehold', household) this.$store.dispatch('unfoldPersonsByHousehold', household)
break break
case 'accompanying_period': case 'accompanying_period':
let course = this.nodes.filter(n => n.id === node)[0] let course = this.nodes.filter(n => n.id === node)[0]
console.log('@@@@@@ event on selected Node', course.id) //console.log('@@@@@@ event on selected Node', course.id)
this.$store.dispatch('unfoldPersonsByCourse', course) this.$store.dispatch('unfoldPersonsByCourse', course)
break break
@ -290,7 +299,7 @@ export default {
} }
let link = data.edges[0] let link = data.edges[0]
let linkType = splitId(link, 'link') let linkType = splitId(link, 'link')
console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data) //console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data)
if (linkType.startsWith('relationship')) { if (linkType.startsWith('relationship')) {
//console.log('linkType relationship') //console.log('linkType relationship')
@ -314,7 +323,7 @@ export default {
}) })
}, },
listenStepsToAddRelationship(person) { listenStepsToAddRelationship(person) {
console.log(' @@> listenStep', this.listenPersonFlag) //console.log(' @@> listenStep', this.listenPersonFlag)
if (this.listenPersonFlag === 'step2') { if (this.listenPersonFlag === 'step2') {
//console.log(' @@> person 2', person) //console.log(' @@> person 2', person)
this.newEdgeData.to = person.id this.newEdgeData.to = person.id
@ -333,7 +342,7 @@ export default {
/// control Layers /// control Layers
toggleLayer(value) { toggleLayer(value) {
let id = value.target.value let id = value.target.value
console.log('@@@@@@ toggle Layer', id) //console.log('@@@@@@ toggle Layer', id)
this.forceUpdateComponent() this.forceUpdateComponent()
if (this.checkedLayers.includes(id)) { if (this.checkedLayers.includes(id)) {
this.removeLayer(id) this.removeLayer(id)
@ -382,7 +391,7 @@ export default {
title: null, title: null,
button: { class: null, text: null, } button: { class: null, text: null, }
} }
console.log('==- reset Form', this.modal.data) //console.log('==- reset Form', this.modal.data)
}, },
getRelationsList() { getRelationsList() {
//console.log('fetch relationsList') //console.log('fetch relationsList')
@ -400,12 +409,16 @@ export default {
let person = this.persons.filter(p => p.id === id) let person = this.persons.filter(p => p.id === id)
return person[0] return person[0]
}, },
getPersonAge(id) {
let person = this.getPerson(id)
return getAge(person)
},
// actions // actions
createRelationship() { createRelationship() {
this.displayHelpMessage = true this.displayHelpMessage = true
this.listenPersonFlag = 'step1' // toggle listener in create link mode this.listenPersonFlag = 'step1' // toggle listener in create link mode
console.log(' @@> switch listener to create link mode:', this.listenPersonFlag) //console.log(' @@> switch listener to create link mode:', this.listenPersonFlag)
}, },
dropRelationship() { dropRelationship() {
//console.log('delete', this.modal.data) //console.log('delete', this.modal.data)
@ -417,13 +430,13 @@ export default {
this.forceUpdateComponent() this.forceUpdateComponent()
}, },
submitRelationship() { submitRelationship() {
console.log('submitRelationship', this.modal.action) //console.log('submitRelationship', this.modal.action)
switch (this.modal.action) { switch (this.modal.action) {
case 'create': case 'create':
return postRelationship(this.modal.data) return postRelationship(this.modal.data)
.then(relationship => new Promise(resolve => { .then(relationship => new Promise(resolve => {
console.log('post relationship response', relationship) //console.log('post relationship response', relationship)
this.$store.dispatch('addLinkFromRelationship', relationship) this.$store.dispatch('addLinkFromRelationship', relationship)
this.modal.showModal = false this.modal.showModal = false
this.resetForm() this.resetForm()
@ -435,7 +448,7 @@ export default {
case 'edit': case 'edit':
return patchRelationship(this.modal.data) return patchRelationship(this.modal.data)
.then(relationship => new Promise(resolve => { .then(relationship => new Promise(resolve => {
console.log('patch relationship response', relationship) //console.log('patch relationship response', relationship)
this.$store.commit('updateLink', relationship) this.$store.commit('updateLink', relationship)
this.modal.showModal = false this.modal.showModal = false
this.resetForm() this.resetForm()
@ -450,39 +463,44 @@ export default {
}, },
// export image // export image
exportCanvasAsImage() { async exportCanvasAsImage() {
const canvas = document.getElementById('visgraph')
.querySelector('canvas')
console.log(canvas)
let link = document.getElementById('exportCanvasBtn') let
link.download = "filiation.png" filename = `filiation_${this.household_id}.jpg`,
mime = 'image/jpeg',
quality = 0.85,
footer = `© Chill ${new Date().getFullYear()}`,
timestamp = `${visMessages.fr.visgraph.relationship_household}${this.household_id}${new Date().toLocaleString()}`
canvas.toBlob(blob => { // resolve toBlob in a Promise
console.log(blob) const getCanvasBlob = canvas => new Promise(resolve => {
link.href = URL.createObjectURL(blob) canvas.toBlob(blob => resolve(blob), mime, quality)
}, 'image/png') })
/* // build image from new temporary canvas
TODO improve feature let tmpCanvas = document.createElement('canvas')
tmpCanvas.width = this.canvas.width
tmpCanvas.height = this.canvas.height
// 1. fonctionne, mais pas de contrôle sur le nom let ctx = tmpCanvas.getContext('2d')
if (canvas && canvas.getContext('2d')) { ctx.beginPath()
let img = canvas.toDataURL('image/png;base64;') ctx.fillStyle = '#fff'
img = img.replace('image/png','image/octet-stream') ctx.fillRect(0, 0, tmpCanvas.width, tmpCanvas.height);
window.open(img, '', 'width=1000, height=1000') ctx.fillStyle = '#9d4600'
ctx.fillText(footer +' — '+ timestamp, 5, tmpCanvas.height - 10)
ctx.drawImage(this.canvas, 0, 0)
return await getCanvasBlob(tmpCanvas)
.then(blob => {
let url = document.createElement("a")
url.download = filename
url.href = window.URL.createObjectURL(blob)
url.click()
console.log('url', url.href)
URL.revokeObjectURL(url.href)
})
} }
// 2. fonctionne, mais 2 click et pas compatible avec tous les browsers
let link = document.getElementById('exportCanvasBtn')
link.download = "image.png"
canvas.toBlob(blob => {
link.href = URL.createObjectURL(blob)
}, 'image/png')
*/
}
} }
} }
</script> </script>

View File

@ -24,6 +24,7 @@ const visMessages = {
refresh: "Rafraîchir", refresh: "Rafraîchir",
screenshot: "Prendre une photo", screenshot: "Prendre une photo",
choose_relation: "Choisissez le lien de parenté", choose_relation: "Choisissez le lien de parenté",
relationship_household: "Filiation du ménage",
}, },
edit: 'Éditer', edit: 'Éditer',
del: 'Supprimer', del: 'Supprimer',

View File

@ -16,7 +16,12 @@ persons.forEach(person => {
}) })
const app = createApp({ const app = createApp({
template: `<app></app>` template: `<app :household_id="this.household_id"></app>`,
data() {
return {
household_id: JSON.parse(container.dataset.householdId)
}
}
}) })
.use(store) .use(store)
.use(i18n) .use(i18n)

View File

@ -112,7 +112,7 @@ const store = createStore({
} }
}) })
//console.log('array', array.map(item => item.person.id)) //console.log('array', array.map(item => item.person.id))
console.log('get persons group', group.map(f => f.id)) //console.log('get persons group', group.map(f => f.id))
return group return group
}, },
@ -120,13 +120,17 @@ const store = createStore({
}, },
mutations: { mutations: {
addPerson(state, [person, options]) { addPerson(state, [person, options]) {
let age = getAge(person)
age = (age === '')? '' : ' - ' + age
let debug = '' let debug = ''
/// Debug mode: uncomment to display person_id on visgraph /// Debug mode: uncomment to display person_id on visgraph
//debug = `\nid ${person.id}` //debug = `\nid ${person.id}`
person.group = person.type person.group = person.type
person._id = person.id person._id = person.id
person.id = `person_${person.id}` person.id = `person_${person.id}`
person.label = `*${person.text}*\n_${getGender(person.gender)} - ${getAge(person.birthdate)}_${debug}` // person.label = `*${person.text}*\n_${getGender(person.gender)}${age}_${debug}`
person.folded = false person.folded = false
// folded is used for missing persons // folded is used for missing persons
if (options.folded) { if (options.folded) {
@ -161,7 +165,7 @@ const store = createStore({
state.links.push(link) state.links.push(link)
}, },
updateLink(state, link) { updateLink(state, link) {
console.log('updateLink', link) //console.log('updateLink', link)
let link_ = { let link_ = {
from: `person_${link.fromPerson.id}`, from: `person_${link.fromPerson.id}`,
to: `person_${link.toPerson.id}`, to: `person_${link.toPerson.id}`,
@ -305,15 +309,16 @@ const store = createStore({
*/ */
addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) { addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) {
let members = getters.getMembersByHousehold(household.id) let members = getters.getMembersByHousehold(household.id)
console.log('add link for', members.length, 'members') //console.log('add link for', members.length, 'members')
members.forEach(m => { members.forEach(m => {
commit('addLink', { commit('addLink', {
from: `${m.person.type}_${m.person.id}`, from: `${m.person.type}_${m.person.id}`,
to: `household_${m.person.current_household_id}`, to: `${household.id}`,
id: `household_${m.person.current_household_id}-person_${m.person.id}`, id: `${household.id}-person_${m.person.id}`,
arrows: 'from', arrows: 'from',
color: 'pink', color: 'pink',
font: { color: '#D04A60' }, font: { color: '#D04A60' },
dashes: (getHouseholdWidth(m) === 1)? [0,4] : false, //edge style: [dash, gap, dash, gap]
label: getHouseholdLabel(m), label: getHouseholdLabel(m),
width: getHouseholdWidth(m), width: getHouseholdWidth(m),
}) })
@ -362,7 +367,7 @@ const store = createStore({
*/ */
addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) { addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) {
const participations = getters.getParticipationsByCourse(course.id) const participations = getters.getParticipationsByCourse(course.id)
console.log('add link for', participations.length, 'participations') //console.log('add link for', participations.length, 'participations')
participations.forEach(p => { participations.forEach(p => {
//console.log(p.person.id) //console.log(p.person.id)
commit('addLink', { commit('addLink', {
@ -445,7 +450,7 @@ const store = createStore({
* @param array * @param array
*/ */
addMissingPerson({ commit, getters, dispatch }, [person, parent]) { addMissingPerson({ commit, getters, dispatch }, [person, parent]) {
console.log('! add missing Person', person.id) //console.log('! add missing Person', person.id)
commit('markPersonLoaded', person.id) commit('markPersonLoaded', person.id)
commit('addPerson', [person, { folded: true }]) commit('addPerson', [person, { folded: true }])
if (getters.isExcludedNode(parent.id)) { if (getters.isExcludedNode(parent.id)) {
@ -467,7 +472,7 @@ const store = createStore({
getters.getPersonsGroup(participations) getters.getPersonsGroup(participations)
.forEach(person => { .forEach(person => {
if (person.folded === true) { if (person.folded === true) {
console.log('-=. unfold and expand person', person.id) //console.log('-=. unfold and expand person', person.id)
commit('unfoldPerson', person) commit('unfoldPerson', person)
dispatch('fetchInfoForPerson', person) dispatch('fetchInfoForPerson', person)
} }
@ -485,7 +490,7 @@ const store = createStore({
getters.getPersonsGroup(members) getters.getPersonsGroup(members)
.forEach(person => { .forEach(person => {
if (person.folded === true) { if (person.folded === true) {
console.log('-=. unfold and expand person', person.id) //console.log('-=. unfold and expand person', person.id)
commit('unfoldPerson', person) commit('unfoldPerson', person)
dispatch('fetchInfoForPerson', person) dispatch('fetchInfoForPerson', person)
} }

View File

@ -13,13 +13,12 @@ window.options = {
locale: 'fr', locale: 'fr',
locales: visMessages, locales: visMessages,
/* /*
*/
configure: { configure: {
enabled: true, enabled: true,
filter: 'nodes,edges', filter: 'physics',
//container: undefined,
showButton: true showButton: true
}, },
*/
physics: { physics: {
enabled: true, enabled: true,
barnesHut: { barnesHut: {
@ -37,8 +36,8 @@ window.options = {
centralGravity: 0.01, centralGravity: 0.01,
springLength: 100, springLength: 100,
springConstant: 0.08, springConstant: 0.08,
damping: 0.4, damping: 0.75,
avoidOverlap: 0 avoidOverlap: 0.00
}, },
repulsion: { repulsion: {
centralGravity: 0.2, centralGravity: 0.2,
@ -159,17 +158,21 @@ const getGender = (gender) => {
} }
/** /**
* TODO Repeat getAge() in PersonRenderBox.vue * TODO only one abstract function (-> getAge() is repeated in PersonRenderBox.vue)
* @param birthdate * @param person
* @returns {string|null} * @returns {string|null}
*/ */
const getAge = (birthdate) => { const getAge = (person) => {
if (null === birthdate) { if (person.birthdate) {
return null let birthdate = new Date(person.birthdate.datetime)
if (person.deathdate) {
let deathdate = new Date(person.deathdate.datetime)
return (deathdate.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
} }
const birthday = new Date(birthdate.datetime) let now = new Date()
const now = new Date() return (now.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
return (now.getFullYear() - birthday.getFullYear()) + ' '+ visMessages.fr.visgraph.years }
return ''
} }
/** /**

View File

@ -192,6 +192,7 @@ export default {
return `/fr/person/${this.person.id}/general`; return `/fr/person/${this.person.id}/general`;
}, },
getAge: function() { getAge: function() {
// TODO only one abstract function
if(this.person.birthdate && !this.person.deathdate){ if(this.person.birthdate && !this.person.deathdate){
const birthday = new Date(this.person.birthdate.datetime) const birthday = new Date(this.person.birthdate.datetime)
const now = new Date() const now = new Date()

View File

@ -17,7 +17,8 @@
<div id="relationship-graph" <div id="relationship-graph"
style="margin-top: -3rem" style="margin-top: -3rem"
data-persons="{{ persons|e('html_attr') }}"> data-persons="{{ persons|e('html_attr') }}"
data-household-id="{{ household.id|e('html_attr') }}">
</div> </div>
</div> </div>
{% endblock %} {% endblock %}