Commit 3fca2db8 authored by Marko Řeháček's avatar Marko Řeháček
Browse files

Store/data: fix dataset validation #27, inform store/interaction about removed...

Store/data: fix dataset validation #27, inform store/interaction about removed nodes, minor refactor
Store/interaction: respond to removed nodes, change unselect mutation
parent 1c50cf96
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -229,7 +229,7 @@ export default {
      'SET_NODE_DETAIL',
      'UNSET_NODE_DETAIL',
      'SELECT_NODE',
      'UNSELECT_NODE',
      'DESELECT_NODES',
      'SET_TOOL_SHOW',
      'SET_LINK_HIGHLIGHT',
      'CLEAR_LINK_HIGHLIGHT'
@@ -257,7 +257,7 @@ export default {
        // if user held the node without dragging, interpret is as a selection
        if (this.currentDragLength < 4) {
          if (this.isUserSelecting) {
            this.isSelected ? this.UNSELECT_NODE(this.node.id) : this.SELECT_NODE(this.node.id)
            this.isSelected ? this.DESELECT_NODES([this.node.id]) : this.SELECT_NODE(this.node.id)
            this.isSelected ? this.showNodeSelection() : this.hideNodeSelectionIfEmpty()
            this.isUserSelecting = false
          }
+88 −42
Original line number Diff line number Diff line
import Vue from 'vue'
import mockData from '@/datasets/data-small.js'
import mockData from '@/datasets/data-harry.js'
import { cloneDeep, merge, sortBy } from 'lodash'
import { clone, validate } from '@babel/types'
import { type } from 'os'

/**
 * @file Manages data storage/operations for network visualization module
@@ -95,9 +96,10 @@ Object.freeze(AN_AGGREGATION_TYPES)
 * @param {Object} state
 * @param {Node[]} state.nodes
 * @param {number} id of node
 * @returns {Node}
 * 
 * @returns {Node} Node instance, or undefined if not found
 */
function getNodeById(state, id) {
function findNodeById(state, id) {
  return state.nodes.find(n => n.id === id)
}

@@ -108,6 +110,7 @@ function filterVisibleLinks(links) {
/**
 * Tell if node is visible in graph visualization
 * @param {Node} node
 * 
 * @returns {boolean}
 */
function isNodeVisible(node) {
@@ -138,6 +141,7 @@ export function findConnectedLinks(links, id) {
/**
 * Check if visible: not in multilink, not an archived multilink, newTarget != newSource
 * @param {Link} l
 * 
 * @returns {boolean}
 */
function isLinkVisible(l) {
@@ -154,6 +158,7 @@ function isLinkVisible(l) {
/**
 * Find first available id by iterating over objects in array
 * @param {Object[]} arr of objects, each object has attribute id
 * 
 * @returns {number} id
 */
function arrGetNewId(arr) {
@@ -166,6 +171,7 @@ function arrGetNewId(arr) {
/**
 * Check if link has set vis.newSource and vis.newTarget
 * @param {Link} link
 * 
 * @returns {boolean}
 */
function isLinkTransferred(link) {
@@ -198,6 +204,7 @@ function isLinkSimilar(link, linkToMatch) {
 * @param {Object[]} arr where link or multilink will be created
 * @param {Link} link to insert
 * @param {Number} groupId so multilink has always source set to group node
 * 
 * @return {Object} multilink copy, if created; null otherwise
 */
function insertUpdatedLink(arr, link, groupId, newId) {
@@ -250,6 +257,8 @@ function insertUpdatedLink(arr, link, groupId, newId) {
/**
 * Return link.vis.newSource uif set, otherwise just link.source
 * @param {Link} link
 * 
 * @return {number} ID of source
 */
export function getLinkSource(link) {
  if (link.vis !== undefined && link.vis.newSource !== undefined)
@@ -258,8 +267,10 @@ export function getLinkSource(link) {
}

/**
 * Return link.vis.newTarget uif set, otherwise just link.target
 * Return link.vis.newTarget if set, otherwise just link.target
 * @param {Link} link
 * 
 * @return {number} ID of target
 */
export function getLinkTarget(link) {
  if (link.vis !== undefined && link.vis.newTarget !== undefined)
@@ -277,7 +288,8 @@ export function getLinkTarget(link) {
 * @param {Link} link to insert; NOT MUTATED
 * @param {Link[]} arr to be updated with the new link
 * @param {number} availableId for creating new multilink
 * @returns multilink, if new one was created during insertion; otherwise null
 * 
 * @returns {Link} multilink, if new one was created during insertion; otherwise null
 */
function insertLinkWithMultilinkUpdate(link, arr, availableId) {
  if (isMultilink(link)) throw Error('Bad usage. Inserting a multilink is not allowed.')
@@ -338,7 +350,7 @@ function multilinkUpdateDirection(/* const */ arr, ml) {
 * @param {Number} id of presentational node
 */
export function isReplacedBy(node, id) {
  return !node.vis ? false : (node.vis.replacedBy === id)
  return !node.vis ? false : node.vis.replacedBy === id
}

/**
@@ -347,33 +359,66 @@ export function isReplacedBy(node, id) {
 * @param {Number} id of presentational node
 */
export function isBundledIn(node, id) {
  return !node.vis ? false : (node.vis.bundledIn === id)
  return !node.vis ? false : node.vis.bundledIn === id
}

export function isBundle(n) {
  return !n.vis ? false : (n.vis.bundledNodes !== undefined && n.vis.bundledNodes.length > 0)
}

/**
 * Check if link is in multilink
 * @param {Link} link
 * @return {Boolean}
 * @return {boolean}
 */
function isInMultilink(link) {
  if (!link.vis) return false
  return link.vis.inMultilink !== undefined
}

/**
 * Check if link is in specific multilink
 * @param {Link} link
 * @param {number} mlId
 * 
 * @return {boolean}
 */
function isInMultilinkOfId(link, mlId) {
  if (!Number.isInteger(mlId)) throw Error('Invalid ID')

  if (!link.vis) return false
  return link.vis.inMultilink === mlId
}

/**
 * Check for vis.isMultilink attribute
 * @param {Link} link
 * 
 * @return {boolean}
 */
function isMultilink(link) {
export function isMultilink(link) {
  if (!link.vis) return false
  return link.vis.isMultilink !== undefined
}

/**
 * Check for vis.isArchived attribute
 * @param {Link} multilink
 * 
 * @return {boolean}
 */
function isMultilinkArchived(multilink) {
  if (!isMultilink(multilink)) return false
  return multilink.vis.isArchived === true
}

/**
 * Push new group node to state
 * @param {Object} state writes to state.nodes
 * @param {Node} node will be assigned type and new ID
 * @param {boolean} isUserDefined true to set this vis attr
 * 
 * @return {number} ID assigned to the new node
 */
function pushNodeToState(state, node, isUserDefined) {
  node.id = arrGetNewId(state.nodes)
@@ -399,11 +444,11 @@ function setVisAttr(el, attr, val) {
/**
 * Set vis attributes to object, merging vis objects
 * @param {Node|Link} el
 * @param {NodeVis|LinkVis} attrObj vis object to merge
 * @param {NodeVis|LinkVis} visAttrs vis object to merge
 */
function setVisAttrs(el, attrObj) {
function setVisAttrs(el, visAttrs) {
  el.vis = el.vis || {}
  Vue.set(el, 'vis', merge(el.vis, attrObj))
  Vue.set(el, 'vis', merge(el.vis, visAttrs))
}

/**
@@ -450,6 +495,7 @@ function createMultilink(newId, source, target) {
 * Nodes map is in form [{id: replacedById | undefined },..]
 * @param {Map} nodesMap created with makeNodesMap
 * @param {number} nodeId on which to start lookup
 * 
 * @returns {number} node id
 */
function findReplacementNode(nodesMap, nodeId) {
@@ -607,17 +653,13 @@ export const mutations = {

    const groupId = action === 'bundle' ? groupNode.id : pushNodeToState(state, groupNode, false)
    if (action === 'bundle') {
      const n = getNodeById(state, groupId)
      const n = findNodeById(state, groupId)
      if (n.vis && n.vis.bundledNodes !== undefined) {
        const newBundledNodes = n.vis.bundledNodes
        nodesToGroupIds.forEach(id => newBundledNodes.push(id))
        //setVisAttr(n, 'bundledNodes', newBundledNodes)
        n.vis = n.vis || {}
        Vue.set(n.vis, 'bundledNodes', newBundledNodes)
        setVisAttr(n, 'bundledNodes', newBundledNodes)
      } else {
        //setVisAttr(n, 'bundledNodes', nodesToGroupIds)
        n.vis = n.vis || {}
        Vue.set(n.vis, 'bundledNodes', nodesToGroupIds)
        setVisAttr(n, 'bundledNodes', nodesToGroupIds)
      }
      arraySetVisAttrs(state.nodes.filter(n => nodesToGroupIds.includes(n.id)), { bundledIn: groupId })
    } else {
@@ -924,10 +966,8 @@ function makeNodesMap(state) {
  return new Map(
    state.nodes.map(n => {
      if (n.vis) {
        if (n.vis.replacedBy !== undefined)
          return [n.id, n.vis.replacedBy]
        if (n.vis.bundledIn !== undefined)
          return [n.id, n.vis.bundledIn]
        if (n.vis.replacedBy !== undefined) return [n.id, n.vis.replacedBy]
        if (n.vis.bundledIn !== undefined) return [n.id, n.vis.bundledIn]
      }
      return [n.id, undefined]
    })
@@ -988,29 +1028,31 @@ export function validateDataset(dataset) {
      const target = getLinkTarget(link)

      const similarLinks = val.links.filter(l => isLinkSimilar(l, link) && l.id !== link.id)
      const multilinks = similarLinks.filter(l => isMultilink(l))
      const multilinks = similarLinks.filter(l => isMultilink(l) && !isMultilinkArchived(l))
      const singlelinks = similarLinks.filter(l => !isMultilink(l))
      debug && console.log("Similar: "+JSON.stringify(similarLinks))
      debug && console.log("   ML: "+JSON.stringify(multilinks))
      debug && console.log("   other SL: "+JSON.stringify(singlelinks))
      // there are similarly connected links
      // there are similarly connected links except the link itself
      if (similarLinks.length > 0) {
        if (source === target) return

        // if its multilink, check that there exists singlelinks which it replaced
        if (isMultilink(link)) {
          if (singlelinks.length === 0)
            throw Error('Multilink '+JSON.stringify(link)+' was found without it\' singlelinks.')
          // the similar link is ml, so there should be at least one other singlelink
          if (!isMultilinkArchived(link) && singlelinks.length === 0)
            throw Error('Multilink '+JSON.stringify(link)+' was found without singlelinks.')
        } else {
          // archived multilink are not in the array
          if (multilinks.length !== 1)
            throw Error("Nodes ["+source+","+target+"] have similar links "+JSON.stringify(similarLinks)+" with wrong multilinks set")
          const ml = multilinks[0]

          if (!isInMultilink(link) || link.vis.inMultilink !== ml.id)
          if (link.vis.inMultilink !== ml.id)
            throw Error("Link "+link.id+" is supposed to be in multilink "+JSON.stringify(ml.id))
        }
      } else {
        if (isMultilink(link) && link.vis.isArchived !== true)
        if (isMultilink(link) && !isMultilinkArchived(link))
          throw Error("Link "+link.id+" is not supposed to be multilink.")
      }
    })
@@ -1037,7 +1079,6 @@ export const actions = {

  updateNodes({ commit }, nodesDiff) {
    commit('UPDATE_NODES', nodesDiff)
    validateDataset(state)
  },

  removeNode({ commit }, nodeId) {
@@ -1045,7 +1086,7 @@ export const actions = {
    validateDataset(state)
  },

  groupNodes({ commit }, nodeIdArr) {
  groupNodes({ commit, dispatch }, nodeIds) {
    let groupNode = {
      name: 'Group of: ',
      type: 'group',
@@ -1054,13 +1095,14 @@ export const actions = {

    commit('AGGREGATE_NODES', {
      action: 'group',
      nodeIds: nodeIdArr,
      nodeIds: nodeIds,
      targetGroupNode: groupNode
    })
    validateDataset(state)
    dispatch('interaction/respondToRemovedNodes', nodeIds, { root: true })
  },

  aliasNodes({ commit }, nodeIdArr) {
  aliasNodes({ commit, dispatch }, nodeIds) {
    let groupNode = {
      name: 'Alias of: ',
      type: 'alias',
@@ -1069,10 +1111,11 @@ export const actions = {

    commit('AGGREGATE_NODES', {
      action: 'alias',
      nodeIds: nodeIdArr,
      nodeIds: nodeIds,
      targetGroupNode: groupNode
    })
    validateDataset(state)
    dispatch('interaction/respondToRemovedNodes', nodeIds, { root: true })
  },

  /**
@@ -1081,7 +1124,7 @@ export const actions = {
   * @param {number} payload.representativeNodeId the "bundle" node
   * @param {number[]} payload.nodeIds to bundle inside the representative node
   */
  bundleNodes({ commit, getters }, payload) {
  bundleNodes({ commit, getters, dispatch }, payload) {
    console.log('bundleNodes: ' + JSON.stringify(payload))
    commit('AGGREGATE_NODES', {
      action: 'bundle',
@@ -1089,9 +1132,10 @@ export const actions = {
      targetGroupNode: getters.getNodeById(payload.representativeNodeId)
    })
    validateDataset(state)
    dispatch('interaction/respondToRemovedNodes', payload.nodeIds, { root: true })
  },

  ungroupNode({ commit, getters }, nodeId) {
  ungroupNode({ commit, getters, dispatch }, nodeId) {
    // unbundle first if bundle
    if (getters.getNodeBundledNodeCount(nodeId) > 0) {
      commit('DISAGGREGATE_NODE', {
@@ -1105,6 +1149,7 @@ export const actions = {
      nodeId: nodeId
    })
    validateDataset(state)
    dispatch('interaction/respondToRemovedNodes', [nodeId], { root: true })
  },

  unbundleNode({ commit }, nodeId) {
@@ -1124,13 +1169,14 @@ export const getters = {
  getVisibleNodes: state => filterVisibleNodes(state.nodes),
  getVisibleLinks: state => filterVisibleLinks(state.links),

  getNodeById: state => id => state.nodes.find(n => n.id === id),
  getLinkById: state => id => state.links.find(n => n.id === id),
  getNodeById: state => id => findNodeById(state, id),
  getLinkById: state => id => state.links.find(l => l.id === id),
  getMultilinkLinks: state => multilinkId => state.links.filter(l => isInMultilinkOfId(l, multilinkId)),

  getLinkIdsConnectedToNode: state => id => findConnectedLinks(state.links, id),
  getNodeBundledNodeCount: (state, getters) => id => {
    const n = getters.getNodeById(id)
    if (!n) throw Error('Node does not exist')
  getNodeBundledNodeCount: state => id => {
    const n = findNodeById(state, id)
    if (!n) return 0
    return n.vis && n.vis.bundledNodes !== undefined ? n.vis.bundledNodes.length : 0
  }
}
+14 −2
Original line number Diff line number Diff line
@@ -46,8 +46,8 @@ export const mutations = {
  SELECT_NODE(state, id) {
    state.selectedNodes.push(id)
  },
  UNSELECT_NODE(state, id) {
    state.selectedNodes = state.selectedNodes.filter(n => n !== id)
  DESELECT_NODES(state, idArr) {
    state.selectedNodes = state.selectedNodes.filter(id => !idArr.includes(id))
  },
  CLEAR_NODE_SELECTION(state) {
    state.selectedNodes = []
@@ -64,6 +64,7 @@ export const mutations = {
  },
  HIDE_LINK_DETAIL(state) {
    state.detailedLink = NaN
    state.shownTools.linkDetail = false
  },

  SET_TOOL_SHOW(state, { tool, bool }) {
@@ -104,6 +105,10 @@ export const actions = {
    if (state.selectedNodes.length !== 0) return
    commit('SET_TOOL_SHOW', { tool: 'nodeSelection', bool: false })
  },
  deselectNode({ commit, dispatch }, id) {
    commit('DESELECT_NODES', [id])
    dispatch('hideNodeSelectionIfEmpty')
  },

  showLinkDetail({ commit }, id) {
    commit('SHOW_LINK_DETAIL', id) 
@@ -126,6 +131,13 @@ export const actions = {
  },
  deactivateDebug({ commit }) {
    commit('SET_DEBUG', false)
  },

  respondToRemovedNodes({ commit, state, dispatch }, removedNodes) {
    commit('DESELECT_NODES', removedNodes)
    if (removedNodes.includes(state.detailedNode)) dispatch('hideNodeDetail')
    if (removedNodes.includes(state.hoveredNode)) dispatch('hideNodeDetail')
    commit('HIDE_LINK_DETAIL')
  }
}