Hello World, Lines

This page covers the basics of creating a line plot, introducing the concepts of

  • instantiation of a line object
  • adding the line object to a 3D axes
  • line resolution
  • functional mapping

General Concepts

For S3Dlib, a Line is defined as a continuous sequence of linear segments with a common vertex at joining segments (a line can contain a single segment with two vertices).

Throughout this tutorial and all the example plots, the construction and display of lines consist of three basic steps.

  1. Define coordinate and coloring, either explicitly or usings functions.
  2. Instantiate line objects and apply mapping methods.
  3. Setup the Matplotlib figure and axes.

These three steps are similar to those described in the Hello World tutorial for surfaces.

Default 3D Line Collection Base Class

The base class for producing a line object is:

s3d.ColorLine3DCollection(vertexCoor, segmIndices)

This constructor produces a collection of lines or for a single line, the collection is of size one. The vertexCoor is a list (or array-like) of x,y,z coordinates ( list or array of 3 floats). The segmIndices argument is the vertex indices for the lines, which is a N list of lines, each line being a list of M segments.

The following is the script for producing a collection of two lines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np
from matplotlib import pyplot as plt
import s3dlib.surface as s3d

#.. Base Class Line

# 1. Define line to examine .....................................

verts = [ [-1,1,1], [1,1,-1], [1,-1,1] , [0,-1,-1], [-1,0,0]  ]
segin = [ [4,1],[0,2,3] ]         # indices for two lines.
#segin = [ [4,1,0,2,3] ]          # indices for one lines.

# 2. Setup and map line .........................................

lines = s3d.ColorLine3DCollection(verts,segin)

# 3. Construct figure, add line and plot ........................

fig = plt.figure(figsize=plt.figaspect(1))
ax = plt.axes(projection='3d')
ax.set_aspect('equal')
s3d.auto_scale(ax,lines)

ax.add_collection3d(lines)

plt.show()

which produces the following plot:

../../_images/line_base1.png

For a single line using the same vertices, only one line is defined in the collection by un-commenting the highlighted code, producing

../../_images/line_base2.png

The line to set the axis limits:

ax.set(xlim=(-1,1), ylim=(-1,1), zlim=(-1,1))

is generally always inserted, over-riding the Matplotlib default axis limits. However, the objects can be used to auto-scale the axes using:

s3d.auto_scale(axes,*obj3d)

where obj3d are the S3Dlib objects, which, for this case is only the line collection object. Auto scaling can often produce plots where the x, y, and z axes are scaled differently. However, using this function can be convenient when the object 3D sizes are unknown prior to plotting.

Single Segmented Line

When a single line object may be constructed from a sequence of vertices, the object can be constructed without the segment indices list. For this case, use:

s3d.SegmentLine(vertexCoor)

A simple example of using just vertex coordinates is that of using a sequence of random walk 3D coordinate locations as shown in the following figure

../../_images/line_rand.png

which was produced using the following code. Note that the auto_scale method was used since the range of the result is initially unknown.

import numpy as np
from matplotlib import pyplot as plt
import s3dlib.surface as s3d

#.. Segmented 'continuous' Line

# 1. Define line to examine ......................................

def rand_line(length,seed):
    # Use Matplotlib 'animated 3D random walk' algorithm.
    np.random.seed(seed)
    line_data = np.empty((3, length))
    line_data[:, 0] = np.random.rand(3)
    for index in range(1, length):
        step = (np.random.rand(3) - 0.5) * 0.1
        line_data[:, index] = line_data[:, index - 1] + step
    return line_data.T  #note: transpose

verts = rand_line(100,3)

# 2. Setup and map line ..........................................

line = s3d.SegmentLine(verts)

# 3. Construct figure, add line, and plot ........................

fig = plt.figure(figsize=plt.figaspect(1))
ax = plt.axes(projection='3d')
ax.set_aspect('equal')
s3d.auto_scale(ax,line)

ax.add_collection3d(line)

plt.show()

Parametric Line

A line object with Cartesian coordinates defined by a single parameter is created using the following code:

s3d.ParametricLine(rez,operation)

A ParametricLine object is a sequence of segments, the number be controlled by the rez argument. The operation is the name of a parametric function having one argument in the domain 0 to 1 and returning a set of 3 X N Cartesian coordinates.

Using the rez argument, each segment is recursively subdivided into two segments. The number of recursions (line resolution) is controlled by the rez.

Using the Matplotlib Parametric Curve example, for a rez of 6:

../../_images/line_param1.png
import numpy as np
from matplotlib import pyplot as plt
import s3dlib.surface as s3d

#.. Parametric Line

# 1. Define function to examine .....................................

def parametric_curve(t) :
    #...   0 < t < 1
    r_0, twists = .25, 4
    z = (2*t -1 ) 
    r = r_0 + (1-r_0)*z**2
    theta = twists*np.pi*z
    x = r*np.sin(theta)
    y = r*np.cos(theta)
    return x,y,z

# 2. Setup and map line .............................................
rez=6
line = s3d.ParametricLine(rez,parametric_curve)

# 3. Construct figure, add line, and plot ...........................

fig = plt.figure(figsize=plt.figaspect(1))
ax = plt.axes(projection='3d')
ax.set_aspect('equal')
ax.set(xlim=(-1,1), ylim=(-1,1), zlim= (-1,1) )

ax.add_collection3d(line)

plt.show()

The following figure shows the progressions to higher resolutions for this example of a ParametricLine, from rez=3 to rez=5

../../_images/line_rez3.png

Line Fade()

To enhance the line visualization in 3D, a sense of depth for the line can be applied to the line transparency using the line method:

line.fade(depth,elev,azim)

where depth is the minimum color alpha transparency of a line segment, which ranges from 0 to 1. The default value for depth is 0. When applied, the segment alphas will vary from 1 to the set depth aligned from the front to back. Alignment is based on the axis view position set by the elev and azim, in degrees. Then default values are 30 and -60 for elev and azim, respectively.

Using the default fade for the previous examples results in the following visualizations of the lines:

../../_images/line_fade.png

Names, Parameterization and Composites

In a similar manner to surface objects, line objects may be added together to form one single ColorLine3DCollection object by simply using the + operator. Also shown in the surface Parametric Functions tutorial, composites object are easily formed by addition, illustrating the parameters controlling the line geometry.

Each of the Line constructors has a named argument ‘name=’, which can also be accessed with setters and getters. As a default, name is an empty string. However, for the ParametricLine object, name will default to the name of the ‘operation’ function, if not a lambda function.

When names are assigned, the axes ‘legend’ will use the line names. The colors displayed in the line legend will be affected by the line shading. When lines are added together to form a composite line, the resulting line has no name until the line name is assigned.

../../_images/line_para_rand3.png
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.patches as mpatches
import s3dlib.surface as s3d

#.. composite versus multiple lines

# 1. Define function to examine .....................................

def parametric_curve(t,N) :
    #...   0 < t < 1
    sc = (2*t -1 )
    r_0, twists = .25, 4
    z = sc 
    r = r_0 + (1-r_0)*z**2
    theta = twists*np.pi*sc
    x = r*np.sin(theta)
    y = r*np.cos(theta)
    return x,y,z + N

def rand_line(length,seed):
    # Use Matplotlib 'Animated 3D Random Walk' algorithm.
    np.random.seed(seed)
    line_data = np.empty((3, length))
    line_data[:, 0] = np.random.rand(3)
    for index in range(1, length):
        step = (np.random.rand(3) - 0.5) * 0.1
        line_data[:, index] = line_data[:, index - 1] + step
    return line_data.T  #note: transpose

verts = lambda s : rand_line(100,s)

fig = plt.figure(figsize=(8,4))

# Axes 1: Parameteric Line composite =======================================
rez=6

line1 = s3d.ParametricLine(rez,lambda t : parametric_curve(t,-.25), color='C0')
line2 = s3d.ParametricLine(rez,lambda t : parametric_curve(t,0.00), color='C1')
line3 = s3d.ParametricLine(rez,lambda t : parametric_curve(t,0.25), color='C2')
line = line1+line2+line3

line.name = "single composite line"
line.shade(0.5).fade(.35)
#.........................
ax1 = fig.add_subplot(121, projection='3d')
ax1.set_aspect('equal')
ax1.set(xlim=(-1,1), ylim=(-1,1), zlim= (-1,1) )
C0_patch = mpatches.Patch(color='C0', label='N = 1')
C1_patch = mpatches.Patch(color='C1', label='N = 2')
C2_patch = mpatches.Patch(color='C2', label='N = 3')
ax1.set_title(line.name)

ax1.add_collection3d(line)  # one line in the collection

ax1.legend(handles=[C0_patch,C1_patch,C2_patch])
# Axes 2 Three Sequence Lines ==========================================

line1 = s3d.SegmentLine(verts(3), color='C0',name='seed: 3')
line2 = s3d.SegmentLine(verts(5), color='C1',name='seed: 5')
line3 = s3d.SegmentLine(verts(6), color='C2',name='seed: 6')
#.........................
ax2 = fig.add_subplot(122, projection='3d')
ax2.set_aspect('equal')
s3d.auto_scale(ax2,line1,line2,line3)
ax2.set_title("three lines")

ax2.add_collection3d(line1)
ax2.add_collection3d(line2)
ax2.add_collection3d(line3)

ax2.legend()
# ===================================================================
fig.tight_layout()
plt.show()

Line Properties

Vertex and segment center coordinates of line objects are accessed using line properties as:

x,y,z = line.vertices

and:

x,y,z = line.segmentcenters

Using the parametric line example, line vertices and segment centers are displayed by using a scatter plot as:

x,y,z = line.vertices
X,Y,Z = line.segmentcenters
ax.scatter(x,y,z,label='vertices',color='b')
ax.scatter(X,Y,Z,label="centers", color='r')
ax.legend()

with the result shown below for a rez of 5.

../../_images/line_vertices.png