jacomyal / sigma.js

A JavaScript library aimed at visualizing graphs of thousands of nodes and edges
https://www.sigmajs.org/
MIT License
11.29k stars 1.59k forks source link

parallel edges #1197

Closed Aaz21rus closed 7 months ago

Aaz21rus commented 2 years ago

Hello. I couldn't figure out how to display parallel edges. Please help me find an example.

jacomyal commented 2 years ago

Hi @Aaz21rus,

Right now, there is unfortunately no way to properly represent parallel edges - and we have no solution in mind. The main reason is that drawing edges as curves is very expensive using WebGL, and will scale way less. Also, collision detection will be very hard to implement as well.

The best solution I can suggest is to aggregate parallel edges into one single edge, that is visually different enough to make it clear that it's aggregated. For instance, in a recent project with labelled directed unweighted edges with sigma.js, I represented aggregated edges as thicker (with a thickness proportional to the number of parallel edges), with the concatenation of the labels and a different color.

Aaz21rus commented 2 years ago

Thanks for the answer. The answer helped me a lot. Question: Is it possible to set the color inside the edge label? For example, the edge label: "1 2 3". Each digit has its own color

jacomyal commented 2 years ago

It is possible, with a custom edge labels renderer. Here is one way to do that:

  1. In your edge reducer, for aggregated edges, set the label as full string that will be displayed (to make sure sigma knows the edge has some label, and also to compute the full text width), but also a customLabels property that stores each string / color pair ([{ str: "1 ", color: "blue" }, { str: "2 ", color: "red" }, { str: "3", color: "green" }] for instance)
  2. Copy the drawEdgeLabel function from sigma in your code, and call it in your sigma settings, like edgeLabelRenderer: myCustomDrawEdgeLabel (as it is done in the demo with the node label renderer, for instance)
  3. In your myCustomDrawEdgeLabel, replace the context.fillText call (here) by some code that draws each string with the proper color:
// [...]
if (edgeData.customLabels) {
  let xOffset = 0;
  edgeData.forEach(({ str, color }) => {
    context.fillStyle = color;
    context.fillText(str, -textLength / 2 + xOffset, edgeData.size / 2 + size);
    xOffset += context.measureText(str).width;
  });
} else {
  context.fillText(label, -textLength / 2, edgeData.size / 2 + size);
}
// [...]

I haven't tested this code so there might me some remaining issues to solve, but that should be mostly it.

apitts commented 2 years ago

@jacomyal How about the option of falling back on canvas for curved edges? That should allow both parallel edges and help with https://github.com/jacomyal/sigma.js/issues/1138. Of course it would be slower than WebGL but I can see some real advantages of having WebGL being the default but then allowing the user to specify canvas node and edge programs if they like (which could be used alongside the WebGL defaults). My reading of the code at the moment is that that is not possible..you can use canvas for labels but not for nodes / edges generally.

Yomguithereal commented 2 years ago

@apitts a workaround for now is to hook yourself on the beforeRender event and draw the edges using canvas on a dedicated context. You can use the getEdgeDisplayData and translation methods to help. There is also a way to create a dummy custom program class whose render method actually draw canvas but this is probably less straightforward.

apitts commented 2 years ago

Thanks @Yomguithereal! It was the custom program class that I was thinking about....but it seemed like the node and edge programs are designed to use WebGL. I will experiment and see how I go! If I use the beforeRender event and hook into that, I'm assuming I should turn off edge rendering in the settings to avoid double rendering? Also, if I do manage to figure out the custom program class approach would you be open to a pull request on it?

Yomguithereal commented 2 years ago

I'm assuming I should turn off edge rendering in the settings to avoid double rendering?

Probably so yes.

Also, if I do manage to figure out the custom program class approach would you be open to a pull request on it?

I am not sure we want to go this way yet, we need to discuss it with @jacomyal

jacomyal commented 2 years ago

First, I think with @Yomguithereal we do not want to go back to sigma as it was, ie. two separate and independant renderers within the same codebase. This version 2 is WebGL only, and won't integrate canvas-based edge rendering in its core.

But opening a door to help people render custom edges with canvas using beforeRender sounds very nice to me. To do this, I think some things are missing though:

With this, you could still use sigma with custom edges rendering, and without too much impact on the codebase.

apitts commented 2 years ago

That makes sense @jacomyal! I can certainly see the advantages of keeping the core of v2 focused on WebGL. I should be able to add a boolean renderEdges setting to take care of item 3 above...once I have I'll submit a pull request.

anpari commented 2 years ago

Hi @jacomyal,

first off, amazing library you guys created here! Very impressive work. I have been using graphs visualization and modeling purposes at work for some time and am very excited about this WebGL implementation.

I wanted to come back to you about your statement on performance:

Right now, there is unfortunately no way to properly represent parallel edges - and we have no solution in mind. The main reason is that drawing edges as curves is very expensive using WebGL, and will scale way less. Also, collision detection will be very hard to implement as well.

I understand your concern about performance issues during rendering, but I have a use case at the moment in which most of my edges should be rendered as straight lines, with few of them (~10%) as curved ones. Due to the low number of curved lines, I would accept a performance penalty.

In short: the availability of curved edges would give library users more tools to work with, and they can decide themselves whether it's worth paying the performance price on that feature.

Just my two cents.

eNtrozx commented 1 year ago

Hi guys, just wanted to know if there's a plan to make it work. @anpari's suggestion sounds about right. I also need curvature only for overlapping edges and there's not a lot of them. I haven't tried it yet but I see resources online for old sigma versions with curvedArrow, will try it for now

dowstreet commented 10 months ago

Hi @jacomyal, @Yomguithereal ,

I've done a little experimenting with an alternative approach for rendering multi-digraphs, and so far it looks promising. Instead of using curved lines, each directed edge is drawn using a straight line with an arrowhead. However, each line is offset from the [source, target] center line by a specified amount - i.e. by computing endpoints for the lines that are offset from the node centers. This way the lines do not overlap, and each can be seen individually.

If you leave the endpoints in the interior of the node 'circle', the math is pretty simple (i.e. normal to the slope of the original line) . Alternatively, if you want to find new coordinates for the line endpoints where each line intersects the node 'circle', so that the arrow tip is in the right place, there is a little more computation involved (but still much less than curved lines, I think).

I am pretty new to webgl, but was able to get a basic example working, and also position the edge labels so that they are (further) offset from the (already) offset lines. Someone with more webgl experience could probably improve on the approach.

One question is the API for how to define the offset distance. This approach only works when the number of parallel edges is not too large relative to the diameter of the node circles. The offset for a given edge can be computed based on a multiple the key id - e.g. 0, 1, 2 - for a given [source, target] pair, and could be defined in terms of percentage of node radius width, or in pixels, etc. (and could perhaps be normalized on a node-pair basis based on the current number of edges between the pair).

I'd be interested in your thoughts on adding this feature, and would be happy to assist with the code if you think it could work (and fit with the rest of the overall design). I have only tested with fairly small graphs, so we would probably want to check the performance as the graphs scale up.

In any case - thanks for your work on this library!

dowstreet commented 9 months ago

@Yomguithereal , @jacomyal ,

I plan to start working on an implementation of the approach outlined in the previous comment. Since this is a decent-sized 'feature', I would appreciate any input you might have on integrating with existing settings - e.g. include logic to automatically compute offsets based on properties of the source-target pair (that are defined by other settings), explicitly expose new 'top-level' settings for parallel edges, or use some combo (use explicit if provided, otherwise fall back to auto-computed).

My thinking is to add code to the v3 branch - is that correct? Is this feature something that might make sense to target for the 3.0 release? (not sure of the planned timeline)

Most importantly - it seemed like some of the other approaches proposed above were not a good fit with the overall design goals of sigma v2 and beyond. Does this approach seem reasonable to you, at least, 'reasonable enough' to spend time on an implementation / pull request?

Thanks!

jacomyal commented 7 months ago

Hello everyone,

We just released @sigma/edge-curve, that will become the easier way to address parallel edges display. You can see a live example here: https://www.sigmajs.org/storybook/?path=/story/edge-curve--parallel-edges