dash14 / v-network-graph

An interactive network graph visualization component for Vue 3

Home Page:https://dash14.github.io/v-network-graph/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

1 Edge 2 Directions, Context Menu w/variables and Passing selected edge/node to function

notsonsi opened this issue · comments

commented

Is it possible to have an edge that has 2 directions so instead of having 2 edges for each direction i would like only 1 edge.

Idk if that makes sense so to better explain, I would like this since each router has a separate connection for each other so one going to node 1 to 2 and another going to node 2 to 1 and having 2 lines for that would look too messy since its a big network.
I havent seen anything for that so I wonder if its even possible in this

And also I wonder if it is possible to make like a small margin between those 2 connections in the middle like a seperation

Oh yeah i dont know if its possible but i also needed an edge that works normally like it just adds another edge to the side of another and another edge type that works like i said above

Also could you help me is it possible to show a variable like the traffic of data (for example the one that is in the site that uses a random number generator) in an edge context menu? and also if its possible to put the edges.name or type or whatever in there as well

commented

Also might as well how would u pass the current selected edge to a function?

Because when i try to do an if statement in for example "color: edge => ( )," it gives an error "Expression expected" Im not sure why? And Im not sure if a function is the solution for this

(edit: I fixed this by adding a ternary function with multiple conditions but I dont think it is optimal so I still wonder if the things above are possibe)

Hi @notsonsi,
Sorry for the late reply.

Is it possible to have an edge that has 2 directions so instead of having 2 edges for each direction i would like only 1 edge.

Is it correct that you want to draw arrows on both ends of one edge?
If so, it can be achieved by setting "arrow" to each type of source and target in the following example.
https://dash14.github.io/v-network-graph/examples/appearance.html#arrow-on-edges

You can also try this example to experiment with various margins between edges.
If that's not what you were looking for, could you please show me a illustration of what you have in mind? It would really help me understand better.

Also could you help me is it possible to show a variable like the traffic of data (for example the one that is in the site that uses a random number generator) in an edge context menu? and also if its possible to put the edges.name or type or whatever in there as well

The showNodeContextMenu argument params in the example below contains an edge field. This is the Edge ID.
if the Edge ID is known, the name, type, etc. can be obtained.
https://dash14.github.io/v-network-graph/examples/event.html#context-menu

function showEdgeContextMenu(params: vNG.EdgeEvent<MouseEvent>) {
  const { edge, event } = params
  console.log(edge) // 🌟 Edge ID

  // Disable browser's default context menu
  event.stopPropagation()
  event.preventDefault()
  if (edgeMenu.value) {
    menuTargetEdges.value = params.summarized ? params.edges : [params.edge]
    showContextMenu(edgeMenu.value, event)
  }
}

Because when i try to do an if statement in for example "color: edge => ( )," it gives an error "Expression expected" Im not sure why? And Im not sure if a function is the solution for this

If you just want to change the style of selected edges, such as color and width, you can do so by setting edge.selected in the configs.
https://dash14.github.io/v-network-graph/reference/configurations.html#:~:text=edge.normal%60%0A%20%20%20%20%20%20%20%20//%20%7D%2C-,selected,-%3A%20%7B%20/*%20same

You can try the following example.
https://dash14.github.io/v-network-graph/examples/appearance.html#specifying-style-configurations-by-values

screenshot

If this is not also the answer you are hoping for, please provide more specific details of what you would like to accomplish and reproducible source code with the issue, and perhaps I can provide a more specific approach.

Best Regards

commented

Is it correct that you want to draw arrows on both ends of one edge?

This is what I meant:
image
I did this by adding a invisible node in the middle but Im not sure if there is a better way to do it

The showNodeContextMenu argument params in the example below contains an edge field. This is the Edge ID. If the Edge ID is known, the name, type, etc. can be obtained.

Yeah but how would I add the selected node in here: ?
<div ref="nodeMenu" class="context-menu"> Name: {{ menuTargetNode }} <div>{{ HERE }}</div> </div>

Also I was wondering how could I add like a save button that would save all changed coordinates of nodes and etc to data.ts if you know I would appreciate that as well thanks ^^

Hi @notsonsi,
Thanks for your reply.
I have written an example. I hope it will be helpful.

<script setup lang="ts">
import * as vNG from "v-network-graph";
import data from "./data";
import { ref } from "vue";

const layouts = ref(data.layouts)

const configs = vNG.defineConfigs({
  node: {
    selectable: true,
    label: {
      visible: false
    },
    normal: {
      color: "#00ef31"
    },
    hover: {
      color: "#00ad24"
    }
  },
  edge: {
    selectable: true,
    margin: 2,
    normal: { color: "#00ef31", width: 5 },
    hover: { color: "#00ad24", width: 6 },
    selected: { color: "#eebb00", width: 6 },
    gap: 10,
  },
});

/**
 * Make `transform` value of the object placing on the edge.
 */
function makeTransform(
  center: vNG.Point,
  edgePos: vNG.EdgePosition,
  scale: number
) {
  const radian = Math.atan2(
    edgePos.target.y - edgePos.source.y,
    edgePos.target.x - edgePos.source.x
  );
  const degree = (radian * 180.0) / Math.PI;

  return [
    `translate(${center.x} ${center.y})`,
    `scale(${scale}, ${scale})`,
    `rotate(${degree})`,
  ].join(" ");
}

// ---------------------------------------------------------------------
// Context menu
// ---------------------------------------------------------------------

function showContextMenu(element: HTMLElement, event: MouseEvent) {
  element.style.left = event.x + "px"
  element.style.top = event.y + "px"
  element.style.visibility = "visible"
  const handler = (event: PointerEvent) => {
    if (!event.target || !element.contains(event.target as HTMLElement)) {
      element.style.visibility = "hidden"
      document.removeEventListener("pointerdown", handler, { capture: true })
    }
  }
  document.addEventListener("pointerdown", handler, { passive: true, capture: true })
}

const nodeMenu = ref<HTMLDivElement>()
const menuTargetNode = ref("")
const menuTargetNodeData = ref("")
function showNodeContextMenu(params: vNG.NodeEvent<MouseEvent>) {
  const { node, event } = params
  // Disable browser's default context menu
  event.stopPropagation()
  event.preventDefault()
  if (nodeMenu.value) {
    menuTargetNode.value = data.nodes[node].name ?? ""
    // Please put in the content you want to output.
    menuTargetNodeData.value = data.nodes[node].data
    showContextMenu(nodeMenu.value, event)
  }
}
const eventHandlers: vNG.EventHandlers = {
  "node:contextmenu": showNodeContextMenu,
}

// ---------------------------------------------------------------------
// Save positions
// ---------------------------------------------------------------------

function saveCoordinates() {
  // Please add a process to save the data somewhere you want.
  console.log(JSON.stringify(layouts.value))
}
</script>

<template>
  <div>
    <v-network-graph
      class="graph"
      :nodes="data.nodes"
      :edges="data.edges"
      v-bind:layouts="layouts"
      :configs="configs"
      :event-handlers="eventHandlers"
    >
      <!-- To use CSS within SVG, use <defs>. -->
      <defs>
        <!-- Cannot use <style> directly due to restrictions of Vue. -->
        <component :is="'style'">
          <!-- prettier-ignore -->
          .markers { pointer-events: none; }
          .marker {
            fill: {{ configs.edge.normal.color }};
            transition: fill 0.1s linear;
            pointer-events: none;
          }
          .hovered .marker {
            fill: {{ configs.edge.hover.color }};
          }
          .selected .marker {
            fill: {{ configs.edge.selected.color }};
          }
          .background {
            stroke: #ffffff;
            stroke-width: {{ configs.edge.normal.width + 1 }};
          }
          .hovered .background {
            stroke-width: {{ configs.edge.hover.width + 1 }};
          }
          .selected .background {
            stroke-width: {{ configs.edge.selected.width + 1 }};
          }
        </component>
      </defs>
      <template #edge-overlay="{ scale, center, position, hovered, selected }">
        <!-- Place the triangle at the center of the edge -->
        <g
          class="markers"
          :class="{ hovered, selected }"
          :transform="makeTransform(center, position, scale)"
        >
          <path d="M-5 0 L5 0" class="background" />
          <polygon class="marker" points="-1,0 -14,-7 -14,7" />
          <polygon class="marker" points="1,0 14,-7 14,7" />
        </g>
      </template>
    </v-network-graph>
    <div ref="nodeMenu" class="context-menu">
      Name: {{ menuTargetNode }}
      <div>{{ menuTargetNodeData }}</div>
    </div>
    <button @click="saveCoordinates">Save coordinates</button>
  </div>
</template>

<style lang="scss" scoped>
.graph {
  width: 400px;
  height: 400px;
  border: 1px solid #aaa;
}
.context-menu {
  width: 180px;
  background-color: #efefef;
  padding: 10px;
  position: fixed;
  visibility: hidden;
  font-size: 12px;
  border: 1px solid #aaa;
  box-shadow: 2px 2px 2px #aaa;
  > div {
    border: 1px dashed #aaa;
    padding: 4px;
    margin-top: 8px;
  }
}
</style>
// data.ts
import { Nodes, Edges, Layouts } from "v-network-graph"

const nodes: Nodes = {
  node0: { name: "N0", data: 10 },
  node1: { name: "N1", data: 20 },
  node2: { name: "N2", data: 30 },
}

const edges: Edges = {
  edge1: { id: "edge1", source: "node0", target: "node1" },
  edge2: { id: "edge2", source: "node1", target: "node2" },
  edge3: { id: "edge4", source: "node2", target: "node3" },
  edge4: { id: "edge5", source: "node0", target: "node2" },
}

const layouts: Layouts = {
  nodes: {
    node0: { x: 80, y: 0 },
    node1: { x: 0, y: 120 },
    node2: { x: 160, y: 120 },
  },
}

export default {
  nodes,
  edges,
  layouts,
}

screenshot

Arrowhead

Unfortunately there is no simple way to achieve what you want to achieve.
I applied the following example to place a white background and arrowhead in the center of the edge.

Place any object on the edge
https://dash14.github.io/v-network-graph/examples/appearance.html#place-any-object-on-the-edge

Context-menu

I created one ref that outputs the content to be displayed and substituted it in the showNodeContextMenu() function.

Save coordinates

The process of saving and loading data varies depending on the desired persistence location. On the server side, this can be achieved through an API, while on the client side, data can be stored in localStorage, etc. In any case, the specifics are outside the scope of this library, hence I will explain only the part where the coordinates are obtained when the button is clicked.
By using v-bind:layouts prop with ref variable, you can obtain coordinate information at any time.

commented

Save coordinates

How would I save only the changed coordinates into the local data.ts file for example?

Context-menu

In this case the number doesn't dynamically update is there any way to do that? Also how would I make that TargetNodeData an array? So that it would output all the numbers in the array when used

Save coordinates

How would I save only the changed coordinates into the local data.ts file for example?

When the program runs in the browser, data.ts is no longer transpiled into JavaScript and no longer exists.
So, in general, you will have to write code to persist the data somewhere and code to read it from the persisted destination.
This is how the web works in general and is not specific to this library. There are many options for persistence destinations, and what you should implement depends on the requirements you want to achieve in your application.
Sorry I can't help you more, please do your own research.

Context-menu

In this case the number doesn't dynamically update is there any way to do that?

If the target is written using Vue's ref, reactive, and computed, it should update dynamically.
In the example above, data.nodes is not reactive, so changing a field in it would not update it. If that is the cause, enclosing it in reactive as shown below may solve the problem.

const nodes: Nodes = reactive({
  node0: { name: "N0", data: 10 },
  node1: { name: "N1", data: 20 },
  node2: { name: "N2", data: 30 },
})

Otherwise, please check to see if the target you want to show changes to is reactive.

commented

Context Menu

const menuTargetNodeTraffic = ref("")
When you say target you mean this correct? How would I make this reactive?

Also you didnt see the edit but here: "How would I make that TargetNodeData an array? So that it would output all the numbers in the array when used" I was wondering about this as well

The target meant menuTargetNodeData and the original data to assign to it.
I do not know when the data you wish to display could be changed, so I can only give you a fumble reply.
In the example, the menuTargetNodeData is assigned at the time when the context menu is displayed. If you want its value to change while displaying the context menu, the display will not be updated dynamically.
It makes no difference whether it is a string or an array.

If you want a specific solution, as I wrote in my first reply, please provide the minimum code that can reproduce what you want to do and what is not working.

However, this is not about v-network-graph, but about Vue, since the display destination is a <div>. As an Issue for this library, I'm sorry, but I don't think further support is desirable.

commented
const edgeMenu = ref<HTMLDivElement>()
const menuTargetEdges = ref<string[]>([])
const menuTargetEdgeTraffic = ref("")
function showEdgeContextMenu(params: vNG.EdgeEvent<MouseEvent>) {
  const { edge, event } = params
  // Disable browser's default context menu
  event.stopPropagation()
  event.preventDefault()
  if (edgeMenu.value) {
    menuTargetEdgeTraffic.value = data.edges[edge].traffic
    menuTargetEdges.value = params.summarized ? params.edges : [params.edge]
    showContextMenu(edgeMenu.value, event)
  }
}

I think this is the last thing Im gonna bother you with.
Im trying to pass "data.edges[edge].traffic" to "menuTargetEdgeTraffic.value" but it gives the error:
"Type 'undefined' cannot be used as an index type."
at "edge" in "data.edges[edge].traffic"
Do you know why this is and how I can do this?

When params.summarized is true, params.edge is not set and is undefined.
This means,

const { edge, event } = params

the above edge would be undefined.
When accessing data.edge[edge].traffic, I think you should also check the value of params.summarized and switch the process to get it. (For example, calculate the sum of the traffic retrieved from all edges in params.edges).