Coordinate Visualization
This example provides interactive 3D visualizations of coordinate transforms.
Overview
Visualizing coordinate transformations helps understand:
Rotation effects: How rotations change frame orientation
Frame relationships: ECEF, ENU, NED orientations
Interpolation paths: Quaternion vs Euler interpolation
Spherical coordinates: Range, azimuth, elevation geometry
Key Concepts
Frame axes: Visualizing coordinate system orientation
Transformation chains: Sequential rotations
Geodetic surface: Earth ellipsoid visualization
Great circles: Shortest paths on sphere
Rotation Axes: Visualizing how rotations affect coordinate frame orientation.
Euler Sequences: ZYX, ZXZ, and other Euler angle conventions produce different rotation paths.
SLERP Interpolation: Spherical linear interpolation provides smooth rotation paths between orientations.
Spherical Coordinates: Converting between Cartesian and spherical coordinate systems.
Code Highlights
The example demonstrates:
3D plotting of coordinate frames on the Earth ellipsoid
Euler angle sequence visualization
Quaternion SLERP animation
Spherical coordinate geometry views
Source Code
1"""
2Interactive 3D Coordinate System Visualization.
3
4This example demonstrates:
51. 3D visualization of coordinate transformations
62. Interactive rotation matrix visualization
73. Quaternion SLERP animation
84. Geodetic to ECEF coordinate plotting
9
10Run with: python examples/coordinate_visualization.py
11"""
12
13import sys
14from pathlib import Path
15
16sys.path.insert(0, str(Path(__file__).parent.parent))
17
18# Output directory for generated plots
19OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
20OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
21
22import os
23
24import numpy as np # noqa: E402
25import plotly.graph_objects as go # noqa: E402
26from plotly.subplots import make_subplots # noqa: E402
27
28from pytcl.coordinate_systems import ( # noqa: E402
29 euler2rotmat,
30 quat2rotmat,
31 rotx,
32 roty,
33 rotz,
34 slerp,
35 sphere2cart,
36)
37from pytcl.navigation import geodetic_to_ecef # noqa: E402
38
39SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
40
41
42def plot_rotation_axes() -> go.Figure:
43 """
44 Visualize how rotation matrices transform coordinate axes.
45
46 Creates an interactive 3D plot showing:
47 - Original coordinate axes (XYZ)
48 - Rotated axes after applying rotx, roty, rotz
49 """
50 fig = make_subplots(
51 rows=1,
52 cols=3,
53 specs=[[{"type": "scene"}, {"type": "scene"}, {"type": "scene"}]],
54 subplot_titles=[
55 "Rotation about X (45°)",
56 "Rotation about Y (45°)",
57 "Rotation about Z (45°)",
58 ],
59 )
60
61 # Original axes
62 _origin = np.array([0, 0, 0]) # noqa: F841
63 x_axis = np.array([1, 0, 0])
64 y_axis = np.array([0, 1, 0])
65 z_axis = np.array([0, 0, 1])
66
67 def add_axes(fig, R, col, original_alpha=0.3):
68 """Add original and rotated axes to subplot."""
69 # Original axes (faded)
70 for axis, color, name in [
71 (x_axis, "red", "X"),
72 (y_axis, "green", "Y"),
73 (z_axis, "blue", "Z"),
74 ]:
75 fig.add_trace(
76 go.Scatter3d(
77 x=[0, axis[0]],
78 y=[0, axis[1]],
79 z=[0, axis[2]],
80 mode="lines",
81 line=dict(color=color, width=3),
82 opacity=original_alpha,
83 name=f"Original {name}",
84 showlegend=(col == 1),
85 ),
86 row=1,
87 col=col,
88 )
89
90 # Rotated axes
91 for axis, color, name in [
92 (x_axis, "red", "X'"),
93 (y_axis, "green", "Y'"),
94 (z_axis, "blue", "Z'"),
95 ]:
96 rotated = R @ axis
97 fig.add_trace(
98 go.Scatter3d(
99 x=[0, rotated[0]],
100 y=[0, rotated[1]],
101 z=[0, rotated[2]],
102 mode="lines+markers",
103 line=dict(color=color, width=5),
104 marker=dict(size=4),
105 name=f"Rotated {name}",
106 showlegend=(col == 1),
107 ),
108 row=1,
109 col=col,
110 )
111
112 # Apply rotations
113 angle = np.pi / 4 # 45 degrees
114 add_axes(fig, rotx(angle), col=1)
115 add_axes(fig, roty(angle), col=2)
116 add_axes(fig, rotz(angle), col=3)
117
118 # Update layout for each scene
119 for i in range(1, 4):
120 fig.update_scenes(
121 dict(
122 xaxis=dict(range=[-1.5, 1.5], title="X"),
123 yaxis=dict(range=[-1.5, 1.5], title="Y"),
124 zaxis=dict(range=[-1.5, 1.5], title="Z"),
125 aspectmode="cube",
126 ),
127 row=1,
128 col=i,
129 )
130
131 fig.update_layout(
132 title="Rotation Matrix Visualization (45° rotations)",
133 width=1400,
134 height=500,
135 )
136
137 return fig
138
139
140def plot_euler_rotation_sequence() -> go.Figure:
141 """
142 Visualize Euler angle rotation sequence (ZYX - aerospace convention).
143
144 Shows how yaw, pitch, roll are applied sequentially.
145 """
146 fig = make_subplots(
147 rows=1,
148 cols=4,
149 specs=[[{"type": "scene"}] * 4],
150 subplot_titles=[
151 "Initial",
152 "After Yaw (Z)",
153 "After Pitch (Y)",
154 "After Roll (X)",
155 ],
156 )
157
158 # Define Euler angles
159 yaw = np.radians(30)
160 pitch = np.radians(20)
161 roll = np.radians(15)
162
163 # Rotation matrices at each stage
164 R0 = np.eye(3)
165 R1 = rotz(yaw)
166 R2 = R1 @ roty(pitch)
167 R3 = R2 @ rotx(roll)
168
169 # Also compute full rotation for comparison
170 R_full = euler2rotmat([yaw, pitch, roll], "ZYX")
171 assert np.allclose(R3, R_full), "Rotation matrices should match"
172
173 # Create a simple airplane shape
174 def airplane_points():
175 """Generate points representing an airplane."""
176 # Fuselage
177 fuselage = np.array(
178 [
179 [1, 0, 0], # nose
180 [-1, 0, 0], # tail
181 ]
182 )
183 # Wings
184 wings = np.array(
185 [
186 [0, 0.8, 0], # left wing
187 [0, -0.8, 0], # right wing
188 ]
189 )
190 # Tail fin
191 tail = np.array(
192 [
193 [-0.8, 0, 0],
194 [-1, 0, 0.3],
195 ]
196 )
197 return fuselage, wings, tail
198
199 def add_airplane(fig, R, col):
200 """Add rotated airplane to subplot."""
201 fuselage, wings, tail = airplane_points()
202
203 # Rotate all points
204 fuselage_rot = (R @ fuselage.T).T
205 wings_rot = (R @ wings.T).T
206 tail_rot = (R @ tail.T).T
207
208 # Fuselage
209 fig.add_trace(
210 go.Scatter3d(
211 x=fuselage_rot[:, 0],
212 y=fuselage_rot[:, 1],
213 z=fuselage_rot[:, 2],
214 mode="lines",
215 line=dict(color="blue", width=8),
216 name="Fuselage",
217 showlegend=(col == 1),
218 ),
219 row=1,
220 col=col,
221 )
222
223 # Wings
224 fig.add_trace(
225 go.Scatter3d(
226 x=wings_rot[:, 0],
227 y=wings_rot[:, 1],
228 z=wings_rot[:, 2],
229 mode="lines",
230 line=dict(color="red", width=6),
231 name="Wings",
232 showlegend=(col == 1),
233 ),
234 row=1,
235 col=col,
236 )
237
238 # Tail
239 fig.add_trace(
240 go.Scatter3d(
241 x=tail_rot[:, 0],
242 y=tail_rot[:, 1],
243 z=tail_rot[:, 2],
244 mode="lines",
245 line=dict(color="green", width=4),
246 name="Tail",
247 showlegend=(col == 1),
248 ),
249 row=1,
250 col=col,
251 )
252
253 # Add airplane at each rotation stage
254 for i, R in enumerate([R0, R1, R2, R3], 1):
255 add_airplane(fig, R, col=i)
256
257 # Update layout
258 for i in range(1, 5):
259 fig.update_scenes(
260 dict(
261 xaxis=dict(range=[-1.5, 1.5], title="X"),
262 yaxis=dict(range=[-1.5, 1.5], title="Y"),
263 zaxis=dict(range=[-1.5, 1.5], title="Z"),
264 aspectmode="cube",
265 camera=dict(eye=dict(x=1.5, y=1.5, z=1.0)),
266 ),
267 row=1,
268 col=i,
269 )
270
271 fig.update_layout(
272 title=(
273 f"Euler Rotation Sequence (ZYX): Yaw={np.degrees(yaw):.0f}°, "
274 f"Pitch={np.degrees(pitch):.0f}°, Roll={np.degrees(roll):.0f}°"
275 ),
276 width=1600,
277 height=500,
278 )
279
280 return fig
281
282
283def plot_quaternion_slerp() -> go.Figure:
284 """
285 Visualize quaternion SLERP interpolation.
286
287 Shows smooth interpolation between two orientations.
288 """
289 fig = go.Figure()
290
291 # Start and end quaternions
292 # Identity (no rotation)
293 q1 = np.array([1, 0, 0, 0])
294
295 # 90 degree rotation about Z axis
296 angle = np.pi / 2
297 q2 = np.array([np.cos(angle / 2), 0, 0, np.sin(angle / 2)])
298
299 # Interpolation steps
300 n_steps = 20
301 t_values = np.linspace(0, 1, n_steps)
302
303 # Colors for interpolation
304 colors = [
305 f"rgb({int(255 * (1 - t))}, {int(100 + 155 * t)}, {int(255 * t)})"
306 for t in t_values
307 ]
308
309 # Reference vector to rotate
310 v = np.array([1, 0, 0])
311
312 # Add interpolated vectors
313 for i, t in enumerate(t_values):
314 q_interp = slerp(q1, q2, t)
315 R = quat2rotmat(q_interp)
316 v_rot = R @ v
317
318 fig.add_trace(
319 go.Scatter3d(
320 x=[0, v_rot[0]],
321 y=[0, v_rot[1]],
322 z=[0, v_rot[2]],
323 mode="lines+markers",
324 line=dict(color=colors[i], width=4),
325 marker=dict(size=4, color=colors[i]),
326 name=f"t={t:.2f}",
327 showlegend=(i % 5 == 0),
328 )
329 )
330
331 # Add arc showing the path
332 arc_points = []
333 for t in np.linspace(0, 1, 100):
334 q_interp = slerp(q1, q2, t)
335 R = quat2rotmat(q_interp)
336 arc_points.append(R @ v)
337 arc_points = np.array(arc_points)
338
339 fig.add_trace(
340 go.Scatter3d(
341 x=arc_points[:, 0],
342 y=arc_points[:, 1],
343 z=arc_points[:, 2],
344 mode="lines",
345 line=dict(color="gray", width=2, dash="dash"),
346 name="SLERP path",
347 )
348 )
349
350 fig.update_layout(
351 title="Quaternion SLERP Interpolation (0° to 90° about Z-axis)",
352 scene=dict(
353 xaxis=dict(range=[-1.5, 1.5], title="X"),
354 yaxis=dict(range=[-1.5, 1.5], title="Y"),
355 zaxis=dict(range=[-1.5, 1.5], title="Z"),
356 aspectmode="cube",
357 ),
358 width=800,
359 height=700,
360 )
361
362 return fig
363
364
365def plot_spherical_coordinates() -> go.Figure:
366 """
367 Visualize spherical coordinate system.
368
369 Shows the relationship between Cartesian and spherical coordinates.
370 """
371 fig = go.Figure()
372
373 # Generate a grid of points on a sphere
374 n_az = 24
375 n_el = 12
376 r = 1.0
377
378 # Azimuth lines (constant azimuth, varying elevation)
379 for az in np.linspace(0, 2 * np.pi, n_az, endpoint=False):
380 el_range = np.linspace(-np.pi / 2, np.pi / 2, 50)
381 points = np.array(
382 [sphere2cart(r, az, el, system_type="az-el") for el in el_range]
383 )
384 fig.add_trace(
385 go.Scatter3d(
386 x=points[:, 0],
387 y=points[:, 1],
388 z=points[:, 2],
389 mode="lines",
390 line=dict(color="lightblue", width=1),
391 showlegend=False,
392 )
393 )
394
395 # Elevation lines (constant elevation, varying azimuth)
396 for el in np.linspace(-np.pi / 2 + 0.1, np.pi / 2 - 0.1, n_el):
397 az_range = np.linspace(0, 2 * np.pi, 50)
398 points = np.array(
399 [sphere2cart(r, az, el, system_type="az-el") for az in az_range]
400 )
401 fig.add_trace(
402 go.Scatter3d(
403 x=points[:, 0],
404 y=points[:, 1],
405 z=points[:, 2],
406 mode="lines",
407 line=dict(color="lightgreen", width=1),
408 showlegend=False,
409 )
410 )
411
412 # Highlight a specific point
413 test_az = np.radians(45)
414 test_el = np.radians(30)
415 test_point = sphere2cart(r, test_az, test_el, system_type="az-el")
416
417 # Add the point
418 fig.add_trace(
419 go.Scatter3d(
420 x=[test_point[0]],
421 y=[test_point[1]],
422 z=[test_point[2]],
423 mode="markers",
424 marker=dict(size=10, color="red"),
425 name=f"Point (az={np.degrees(test_az):.0f}°, el={np.degrees(test_el):.0f}°)",
426 )
427 )
428
429 # Add lines showing the coordinates
430 # Line from origin to projection on xy-plane
431 proj_xy = np.array([test_point[0], test_point[1], 0])
432 fig.add_trace(
433 go.Scatter3d(
434 x=[0, proj_xy[0]],
435 y=[0, proj_xy[1]],
436 z=[0, 0],
437 mode="lines",
438 line=dict(color="blue", width=3, dash="dash"),
439 name="XY projection",
440 )
441 )
442
443 # Line from projection to point (showing elevation)
444 fig.add_trace(
445 go.Scatter3d(
446 x=[proj_xy[0], test_point[0]],
447 y=[proj_xy[1], test_point[1]],
448 z=[0, test_point[2]],
449 mode="lines",
450 line=dict(color="green", width=3, dash="dash"),
451 name="Elevation",
452 )
453 )
454
455 # Line from origin to point (range)
456 fig.add_trace(
457 go.Scatter3d(
458 x=[0, test_point[0]],
459 y=[0, test_point[1]],
460 z=[0, test_point[2]],
461 mode="lines",
462 line=dict(color="red", width=3),
463 name="Range vector",
464 )
465 )
466
467 # Add coordinate axes
468 axis_len = 1.3
469 for axis, color, name in [
470 ([axis_len, 0, 0], "red", "X"),
471 ([0, axis_len, 0], "green", "Y"),
472 ([0, 0, axis_len], "blue", "Z"),
473 ]:
474 fig.add_trace(
475 go.Scatter3d(
476 x=[0, axis[0]],
477 y=[0, axis[1]],
478 z=[0, axis[2]],
479 mode="lines+text",
480 line=dict(color=color, width=4),
481 text=["", name],
482 textposition="top center",
483 name=f"{name}-axis",
484 showlegend=False,
485 )
486 )
487
488 fig.update_layout(
489 title="Spherical Coordinate System (az-el convention)",
490 scene=dict(
491 xaxis=dict(range=[-1.5, 1.5], title="X"),
492 yaxis=dict(range=[-1.5, 1.5], title="Y"),
493 zaxis=dict(range=[-1.5, 1.5], title="Z"),
494 aspectmode="cube",
495 ),
496 width=900,
497 height=800,
498 )
499
500 return fig
501
502
503def plot_earth_coordinates() -> go.Figure:
504 """
505 Visualize geodetic coordinates on Earth.
506
507 Shows major cities and their ECEF coordinates.
508 """
509 fig = go.Figure()
510
511 # Create a sphere representing Earth
512 u = np.linspace(0, 2 * np.pi, 100)
513 v = np.linspace(0, np.pi, 50)
514 R_earth = 6371000 # meters (approximate)
515 scale = 1e-6 # Scale to make numbers manageable
516
517 x = R_earth * scale * np.outer(np.cos(u), np.sin(v))
518 y = R_earth * scale * np.outer(np.sin(u), np.sin(v))
519 z = R_earth * scale * np.outer(np.ones(np.size(u)), np.cos(v))
520
521 fig.add_trace(
522 go.Surface(
523 x=x,
524 y=y,
525 z=z,
526 colorscale=[[0, "lightblue"], [1, "lightblue"]],
527 opacity=0.6,
528 showscale=False,
529 name="Earth",
530 )
531 )
532
533 # Major cities with their geodetic coordinates
534 cities = {
535 "New York": (40.7128, -74.0060),
536 "London": (51.5074, -0.1278),
537 "Tokyo": (35.6762, 139.6503),
538 "Sydney": (-33.8688, 151.2093),
539 "São Paulo": (-23.5505, -46.6333),
540 "Cairo": (30.0444, 31.2357),
541 }
542
543 # Convert to ECEF and plot
544 city_x, city_y, city_z = [], [], []
545 city_names = []
546
547 for name, (lat_deg, lon_deg) in cities.items():
548 lat = np.radians(lat_deg)
549 lon = np.radians(lon_deg)
550 ecef = geodetic_to_ecef(lat, lon, 0)
551 city_x.append(ecef[0] * scale)
552 city_y.append(ecef[1] * scale)
553 city_z.append(ecef[2] * scale)
554 city_names.append(name)
555
556 fig.add_trace(
557 go.Scatter3d(
558 x=city_x,
559 y=city_y,
560 z=city_z,
561 mode="markers+text",
562 marker=dict(size=8, color="red"),
563 text=city_names,
564 textposition="top center",
565 name="Cities",
566 )
567 )
568
569 # Add equator
570 eq_lon = np.linspace(0, 2 * np.pi, 100)
571 eq_ecef = np.array([geodetic_to_ecef(0, lon, 0) for lon in eq_lon])
572 fig.add_trace(
573 go.Scatter3d(
574 x=eq_ecef[:, 0] * scale,
575 y=eq_ecef[:, 1] * scale,
576 z=eq_ecef[:, 2] * scale,
577 mode="lines",
578 line=dict(color="yellow", width=3),
579 name="Equator",
580 )
581 )
582
583 # Add prime meridian
584 pm_lat = np.linspace(-np.pi / 2, np.pi / 2, 100)
585 pm_ecef = np.array([geodetic_to_ecef(lat, 0, 0) for lat in pm_lat])
586 fig.add_trace(
587 go.Scatter3d(
588 x=pm_ecef[:, 0] * scale,
589 y=pm_ecef[:, 1] * scale,
590 z=pm_ecef[:, 2] * scale,
591 mode="lines",
592 line=dict(color="orange", width=3),
593 name="Prime Meridian",
594 )
595 )
596
597 fig.update_layout(
598 title="Earth: Geodetic to ECEF Coordinate Conversion",
599 scene=dict(
600 xaxis=dict(title="X (1000 km)"),
601 yaxis=dict(title="Y (1000 km)"),
602 zaxis=dict(title="Z (1000 km)"),
603 aspectmode="data",
604 ),
605 width=900,
606 height=800,
607 )
608
609 return fig
610
611
612def main():
613 """Run coordinate visualization examples."""
614 print("Coordinate System Visualization Examples")
615 print("=" * 50)
616
617 # 1. Rotation axes
618 print("\n1. Generating rotation axes visualization...")
619 fig1 = plot_rotation_axes()
620 fig1.write_html(
621 str(OUTPUT_DIR / "coord_viz_rotation_axes.html"),
622 include_plotlyjs="cdn",
623 div_id="coord_viz_rotation_axes",
624 )
625 print(f" Saved to {OUTPUT_DIR / 'coord_viz_rotation_axes.html'}")
626
627 # 2. Euler rotation sequence
628 print("\n2. Generating Euler rotation sequence...")
629 fig2 = plot_euler_rotation_sequence()
630 fig2.write_html(
631 str(OUTPUT_DIR / "coord_viz_euler_sequence.html"),
632 include_plotlyjs="cdn",
633 div_id="coord_viz_euler_sequence",
634 )
635 print(f" Saved to {OUTPUT_DIR / 'coord_viz_euler_sequence.html'}")
636
637 # 3. Quaternion SLERP
638 print("\n3. Generating quaternion SLERP visualization...")
639 fig3 = plot_quaternion_slerp()
640 fig3.write_html(
641 str(OUTPUT_DIR / "coord_viz_slerp.html"),
642 include_plotlyjs="cdn",
643 div_id="coord_viz_slerp",
644 )
645 print(f" Saved to {OUTPUT_DIR / 'coord_viz_slerp.html'}")
646
647 # 4. Spherical coordinates
648 print("\n4. Generating spherical coordinates visualization...")
649 fig4 = plot_spherical_coordinates()
650 fig4.write_html(
651 str(OUTPUT_DIR / "coord_viz_spherical.html"),
652 include_plotlyjs="cdn",
653 div_id="coord_viz_spherical",
654 )
655 print(f" Saved to {OUTPUT_DIR / 'coord_viz_spherical.html'}")
656
657 # 5. Earth coordinates
658 print("\n5. Generating Earth coordinates visualization...")
659 fig5 = plot_earth_coordinates()
660 fig5.write_html(
661 str(OUTPUT_DIR / "coord_viz_earth.html"),
662 include_plotlyjs="cdn",
663 div_id="coord_viz_earth",
664 )
665 print(f" Saved to {OUTPUT_DIR / 'coord_viz_earth.html'}")
666
667 # Show all figures
668 print("\nOpening visualizations in browser...")
669 if SHOW_PLOTS:
670 fig1.show()
671 if SHOW_PLOTS:
672 fig2.show()
673 if SHOW_PLOTS:
674 fig3.show()
675 if SHOW_PLOTS:
676 fig4.show()
677 if SHOW_PLOTS:
678 fig5.show()
679
680 print("\nDone!")
681
682
683if __name__ == "__main__":
684 main()
Running the Example
python examples/coordinate_visualization.py
See Also
Coordinate Systems - Coordinate conversion functions
Navigation and Geodesy - Geodetic operations