Plotting 3D points connected by arbitrary lines (i.e. a 3D graph)

I’m brand new to Plotly, but want to try it out for 3D interactive plotting because I hear it’s way faster than matplotlib for that use case. I use Jupyter notebooks and so far have only used matplotlib through ipympl.

What I want to do is to plot a set of connected line segments in 3D (or a “forest” of such connected components). The thing that differentiates this from a typical line graph is that the network is arbitrarily branched, i.e. there is not some linear ordering of the points such that point n is always connected only to points n-1 and n+1. Think of it as a 3D graph (the graph theory kind).

When using matplotlib I set up a set of 3D axes, then do a bunch of ax.plot([x1,x2],[y1,y2],[z1,z2]) calls for each segment. There may be hundreds of these, each with its own color computed programmatically on the fly, and I plot them as my code generates them. The resulting plots are very slow to rotate interactively, and use the entirety of the notebook’s resources to do so, which seems from my research to be par for the course with matplotlib.

I’m trying to “port” this to Plotly to improve performance. However, looking at all the examples on the Plotly website, I get the strong impression that Plotly really wants a whole figure “batched” into a single, highly vectorized data structure that can be passed to it in one go. So I suspect that for my case it wants something like an OpenGL vertex buffer/index buffer pair, in other words all vertices placed with their X coordinates in one big array, and same for Y and Z, and then a list of all the edges somehow expressed as pairs of indices into these big arrays.

However, I can’t find an example of the syntax for this. The example line graphs are of the kind where there is one single line snaking through the points in a well-defined ordering, or there are scatter plots that are JUST a cloud of vertices with no edges.

Welcome to the forums, @bioplots35.

I’m having trouble understanding of what you are looking for. Do you have a sample image of what the graph should look like? And maybe sample data?

Well, it’s ultimately based on molecules, so if you look at the “wireframe” models here that gives you some sort of idea:

https://www.chm.bris.ac.uk/~paulmay/temp/pcc/displaying3d.htm

Yes, I know there already are 3D molecule viewers and I’ve used them, but I want more customizability than they give me (nglview allows custom 3D line objects to be added, but that slows it down to at least as slow as matplotlib) and I don’t need fancy spacefilling/3D lighting. I just have the (x,y,z) triples for each atom and a list of which pairs are connected (I don’t mean necessarily a Python list in terms of formatting, what I mean is that the code is structured as a loop that generates all the connections one by one). And unlike in those models where the coloring is by vertex (i.e. atom), with the bonds “split in half” colorwise and all halves with the same vertex are the same color, in my case it’s the lines that are each colored with a different solid color for their entire length .

If you’re too unfamiliar with molecules for that to help, just imagine a constellation of stars where if you connect certain pairs of them with straight lines it forms a picture. The pairs are arbitrary though–-one shouldn’t assume anything about the degree of any particular vertex or which other vertices it needs to be linked with.

Am I correct in my assessment that the “flavor” of Plotly is not to plot a series of segments one by one, but to somehow package the entire data set in one function call?

In my head this sounds like a 3D version of this:

Or complete decoupled from the networkx:

import plotly.graph_objects as go
import itertools

# Define vertices
vertices = [
    (1, 0, 0), (-1, 0, 0),
    (0, 1, 0), (0, -1, 0),
    (0, 0, 1), (0, 0, -1)
]

# Function to determine if two vertices form an edge
def is_edge(v1, v2):
    diff = sum(abs(a - b) for a, b in zip(v1, v2))
    return diff == 2  # octahedron edge condition

# Build edge traces
edge_traces = []
for v1, v2 in itertools.combinations(vertices, 2):
    if is_edge(v1, v2):
        edge_traces.append(
            go.Scatter3d(
                x=[v1[0], v2[0]],
                y=[v1[1], v2[1]],
                z=[v1[2], v2[2]],
                mode="lines",
                line=dict(color="black", width=4),
                hoverinfo="none"
            )
        )

# Vertex trace
x, y, z = zip(*vertices)
vertex_trace = go.Scatter3d(
    x=x,
    y=y,
    z=z,
    mode="markers",
    marker=dict(
        size=8,
        color="red",
        symbol="circle"
    ),
    name="Vertices"
)

# Create figure
fig = go.Figure(data=edge_traces + [vertex_trace])

fig.update_layout(
    scene=dict(
        xaxis=dict(visible=False),
        yaxis=dict(visible=False),
        zaxis=dict(visible=False),
        aspectmode="data"
    ),
    margin=dict(l=0, r=0, t=0, b=0)
)

fig.show()

Effectively yes, although the point don’t need to be emphasized, only the connecting lines (since two segments entering any point are never collinear in 3D, it’s sufficiently clear where the points would be simply because it’s a junction or “corner”, and explicitly showing them as thicker than the lines would only serve to increase clutter).

Looking at the code you just included, then it seems that only the edge traces are needed, and that the color for each line is passed into the “line = “, i.e. like so

line=dict(color=(r,g,b), width=4)

since the colors are calculated for each edge in code at the time that it’s appended, they aren’t known until v1 and v2 are calculated.

From your plot it seems like a legend is created that shows a list of all the edges, I assume there’s a way to prevent that?