flightplotting 0.2.13__tar.gz → 0.2.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {flightplotting-0.2.13 → flightplotting-0.2.14}/PKG-INFO +2 -4
  2. {flightplotting-0.2.13 → flightplotting-0.2.14}/pyproject.toml +2 -4
  3. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/__init__.py +1 -0
  4. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/plots.py +167 -11
  5. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/traces.py +2 -2
  6. {flightplotting-0.2.13 → flightplotting-0.2.14}/.github/workflows/publish_pypi.yml +0 -0
  7. {flightplotting-0.2.13 → flightplotting-0.2.14}/.gitignore +0 -0
  8. {flightplotting-0.2.13 → flightplotting-0.2.14}/.vscode/settings.json +0 -0
  9. {flightplotting-0.2.13 → flightplotting-0.2.14}/COPYING +0 -0
  10. {flightplotting-0.2.13 → flightplotting-0.2.14}/MANIFEST.in +0 -0
  11. {flightplotting-0.2.13 → flightplotting-0.2.14}/README.md +0 -0
  12. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/data/ColdDraftF3APlane.obj +0 -0
  13. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/data/__init__.py +0 -0
  14. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/model.py +0 -0
  15. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/py.typed +0 -0
  16. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/templates.py +0 -0
  17. {flightplotting-0.2.13 → flightplotting-0.2.14}/src/plotting/titlerenderer.py +0 -0
  18. {flightplotting-0.2.13 → flightplotting-0.2.14}/tests/__init__.py +0 -0
  19. {flightplotting-0.2.13 → flightplotting-0.2.14}/tests/data/__init__.py +0 -0
  20. {flightplotting-0.2.13 → flightplotting-0.2.14}/tests/data/p23_flight.json +0 -0
  21. {flightplotting-0.2.13 → flightplotting-0.2.14}/tests/test_plots.py +0 -0
  22. {flightplotting-0.2.13 → flightplotting-0.2.14}/uv.lock +0 -0
@@ -1,15 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flightplotting
3
- Version: 0.2.13
3
+ Version: 0.2.14
4
4
  Summary: Add your description here
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License-File: COPYING
7
7
  Requires-Python: >=3.12
8
- Requires-Dist: flightdata>=0.3.0
8
+ Requires-Dist: flightdata>=0.3.5
9
9
  Requires-Dist: numpy>=2.1.3
10
10
  Requires-Dist: pandas>=2.2.3
11
- Requires-Dist: pfc-geometry
12
- Requires-Dist: pfcschemas
13
11
  Requires-Dist: plotly>=5.24.1
14
12
  Description-Content-Type: text/markdown
15
13
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flightplotting"
3
- version="v0.2.13"
3
+ version="v0.2.14"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Thomas David", email = "thomasdavid0@gmail.com" }]
@@ -9,9 +9,7 @@ dependencies = [
9
9
  "numpy>=2.1.3",
10
10
  "pandas>=2.2.3",
11
11
  "plotly>=5.24.1",
12
- "flightdata>=0.3.0",
13
- "pfc-geometry",
14
- "pfcschemas",
12
+ "flightdata>=0.3.5",
15
13
  ]
16
14
 
17
15
  [build-system]
@@ -4,6 +4,7 @@ from plotting.plots import (
4
4
  plotdtw,
5
5
  create_3d_plot,
6
6
  plot_regions,
7
+ resize_3d_fig
7
8
  )
8
9
  from plotting.traces import (
9
10
  axestrace,
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import plotly.graph_objects as go
2
3
  from plotly.subplots import make_subplots
3
4
  import plotly.express as px
@@ -15,14 +16,18 @@ from plotting.traces import (
15
16
  )
16
17
 
17
18
  from flightdata import State
18
- from geometry import Coord
19
+ import geometry as g
19
20
  from plotting.model import obj
20
21
  import numpy.typing as npt
21
22
  import numpy as np
22
23
  import pandas as pd
23
- from typing import List, Union
24
+ from typing import List, Union, Callable, Literal
24
25
 
25
26
 
27
+
28
+ def get_colour(i):
29
+ return px.colors.qualitative.Plotly[i % len(px.colors.qualitative.Plotly)]
30
+
26
31
  def plotsec(
27
32
  secs: State | list[State] | dict[str, State],
28
33
  scale=5,
@@ -37,6 +42,8 @@ def plotsec(
37
42
  tips: bool = True,
38
43
  ribbonhover="t",
39
44
  origin=False,
45
+ line=None,
46
+ modelscale=1,
40
47
  ):
41
48
  traces = []
42
49
  keys = None
@@ -50,24 +57,32 @@ def plotsec(
50
57
  else:
51
58
  keys = list(range(len(secs)))
52
59
  showkeys = False
60
+
61
+ def _get_colour(i):
62
+ if isinstance(color, list):
63
+ return color[i % len(color)]
64
+ elif isinstance(color, str):
65
+ return color
66
+ else:
67
+ return get_colour(i)
53
68
 
54
69
  for i, sec in enumerate(secs):
55
70
  text = sec.data.t # - sec.data.t.iloc[0]
56
- _color = color if color is not None else px.colors.qualitative.Plotly[i]
71
+
57
72
  if ribb:
58
- traces += ribbon(sec, 0.5 * scale * 1.85, "grey", name=keys[i], opacity=0.5, hover=ribbonhover)
73
+ traces += ribbon(sec, 0.5 * scale * 1.85, _get_colour(i), name=keys[i], opacity=0.5, hover=ribbonhover)
59
74
  if tips:
60
- traces += tiptrace(sec, scale * 1.85, text=text, name=keys[i])
75
+ traces += tiptrace(sec, scale * 1.85, text=text, name=keys[i], line=({} if line is None else line))
61
76
  if nmodels > 0:
62
- traces += meshes(nmodels, sec, _color, scale)
77
+ traces += meshes(nmodels, sec, _get_colour(i), scale * modelscale)
63
78
  if cg:
64
79
  traces.append(
65
- cgtrace(sec, line=dict(color=_color, width=2), name=keys[i], text=text)
80
+ cgtrace(sec, line=dict(color=_get_colour(i), width=2) | ({} if line is None else line), name=keys[i], text=text)
66
81
  )
67
82
 
68
83
  if origin:
69
- traces += axestrace(Coord.zero(), 50)
70
-
84
+ traces += axestrace(g.Coord.zero(), 50)
85
+
71
86
  if showkeys:
72
87
  for i, key in enumerate(keys):
73
88
  traces.append(
@@ -76,7 +91,7 @@ def plotsec(
76
91
  y=[],
77
92
  z=[],
78
93
  mode="markers",
79
- marker=dict(size=5, color=px.colors.qualitative.Plotly[i]),
94
+ marker=dict(size=5, color=_get_colour(i)),
80
95
  name=key,
81
96
  showlegend=True,
82
97
  )
@@ -85,7 +100,7 @@ def plotsec(
85
100
  if fig is None:
86
101
  fig = go.Figure(
87
102
  data=traces,
88
- layout=go.Layout(template="flight3d+judge_view", uirevision="foo"),
103
+ layout=go.Layout(template="flight3d", uirevision="foo"),
89
104
  )
90
105
  if show_axes:
91
106
  fig.update_layout(
@@ -359,3 +374,144 @@ axis = dict(
359
374
  zerolinecolor="lightgrey",
360
375
  showline=True,
361
376
  )
377
+
378
+
379
+ def create_ortho_state(st: State, axis: Literal['x', 'z'], width: g.Point, gap: g.Point) -> State:
380
+ """rotate by 90 degrees about the given axis,
381
+ then move to where it should be in an orthographic projection.
382
+ assumes front view is along the Y axis (x right, z up).
383
+ also assumes state is centered at the origin.
384
+ """
385
+ st = st.move(g.Transformation(g.Euler(
386
+ np.pi/2 if axis=="x" else 0,
387
+ 0,
388
+ np.pi/2 if axis=="z" else 0
389
+ )))
390
+
391
+ if axis == 'x':
392
+ shift = g.PZ((width.y + width.z) / 2 + gap)
393
+ elif axis == 'z':
394
+ shift = g.PX(-(width.x + width.y) / 2 - gap)
395
+
396
+ st = st.move(g.Transformation( shift) ) # move back to center and offset by shift
397
+
398
+ return st
399
+
400
+
401
+ def applysts(st: State | list[State] | dict[str, State], fun: Callable[[State], State]) -> Union[State, List[State], dict[str, State]]:
402
+ """apply a transformation to all states in a list or dict of states"""
403
+ if isinstance(st, State):
404
+ return fun(st)
405
+ elif isinstance(st, list):
406
+ return [fun(s) for s in st]
407
+ elif isinstance(st, dict):
408
+ return {k: fun(v) for k, v in st.items()}
409
+
410
+
411
+ def get_points(fig: go.Figure) -> g.Point:
412
+ """extract all points from a figure"""
413
+ ps = []
414
+ for d in fig.data:
415
+ try:
416
+ ps.append(g.Point(d.x, d.y, d.z))
417
+ except Exception:
418
+ pass
419
+ return g.Point.concatenate(ps)
420
+
421
+
422
+ def plot_3view(st: State | list[State] | dict[str, State], plotfun: Callable, gap: float, legend_vstep=10):
423
+
424
+ allsts = State.stack(st, "grp") if not isinstance(st, State) else st
425
+
426
+ width = allsts.pos.max() - allsts.pos.min()
427
+ center = allsts.pos.min() + width / 2
428
+
429
+ st0 = applysts(st, lambda s: s.move(g.Transformation(-center)))
430
+ st1 = applysts(st0, lambda s: create_ortho_state(s, 'x', width, gap))
431
+ st2 = applysts(st0, lambda s: create_ortho_state(s, 'z', width, gap))
432
+
433
+
434
+ fig = go.Figure(data=
435
+ plotfun(st0) + plotfun(st1) + plotfun(st2)
436
+ )
437
+
438
+ anprops = dict(
439
+ showarrow=False,
440
+ font=dict(size=16, family="Rockwell"),
441
+ xanchor="center",
442
+ yanchor="middle",
443
+ )
444
+
445
+
446
+ fig = fig.update_layout(
447
+ template="plotly_white",
448
+ scene=dict(
449
+ camera=dict(
450
+ eye=dict(x=0, y=-1, z=0),
451
+ center=dict(x=0, y=0, z=0),
452
+ projection=dict(type="orthographic"),
453
+ ),
454
+ xaxis=dict(visible=False),
455
+ yaxis=dict(visible=False),
456
+ zaxis=dict(visible=False),
457
+ annotations=[
458
+ dict(
459
+ x=0, y=0, z=-width.z[0] / 2 - gap/2,
460
+ text="Front View",
461
+
462
+ ) | anprops,
463
+ dict(
464
+ x=0, y=0, z=width.z[0] / 2 + width.y[0] / 2 + gap/2,
465
+ text="Top View",
466
+ ) | anprops
467
+ ,
468
+ dict(
469
+ x=-width.x[0] / 2 - width.y[0] / 2 - gap, y=0, z=-width.z[0] / 2 - gap/2,
470
+ text="Left View",
471
+ ) | anprops
472
+ ] + ([] if not isinstance(st, dict) else [dict(
473
+ x=-width.x[0] / 2 - width.y[0] / 2 - gap,
474
+ y=0,
475
+ z=width.z[0] / 2 + width.y[0] / 2 + gap/2 + i * legend_vstep,
476
+ text=k,
477
+ font=dict(size=16, family="Rockwell", color=px.colors.qualitative.Plotly[i]),
478
+ showarrow=False,
479
+ ) for i, k in enumerate(st.keys())] )
480
+ ),
481
+ margin=dict(l=0, r=0, b=0, t=0),
482
+
483
+ )
484
+
485
+ return resize_3d_fig(fig, 600, False)
486
+
487
+
488
+ def resize_3d_fig(fig: go.Figure, width: int | None, width_is_height: bool=False, scale: float=1):
489
+ """Resize a figure to the given width, height and zoom level.
490
+ preserves the aspect ratio of the scene.
491
+ Assumes view is in the positive Y direction
492
+ """
493
+
494
+ all_points = get_points(fig)
495
+
496
+ btm_left = all_points.min()
497
+ top_right = all_points.max()
498
+
499
+ bb = (top_right - btm_left)
500
+ width = width or (fig.layout.height if width_is_height else fig.layout.width) or 600
501
+ zoom = (0.008 * scale*width/(bb.z[0] if width_is_height else bb.x[0]))
502
+ ar = bb * zoom
503
+ height = ar.x[0] * width / ar.z[0] if width_is_height else ar.z[0] * width / ar.x[0]
504
+ fig.update_layout(
505
+ width=height if width_is_height else width,
506
+ height=width if width_is_height else height,
507
+ scene=dict(
508
+ aspectratio=dict(x=ar.x[0], y=ar.y[0], z=ar.z[0]),
509
+ camera=dict(
510
+ eye=dict(x=0, y=-1, z=0),
511
+ center=dict(x=0, y=0, z=0),
512
+ projection=dict(type="orthographic"),
513
+ )
514
+ ),
515
+ )
516
+
517
+ return fig
@@ -107,12 +107,12 @@ def elementtraces(manoeuvre, sec: State):
107
107
 
108
108
 
109
109
 
110
- def tiptrace(seq, span, **kwargs):
110
+ def tiptrace(seq, span, line=None, **kwargs):
111
111
 
112
112
  def make_offset_trace(pos, colour):
113
113
  tr = trace3d(
114
114
  *seq.body_to_world(pos).data.T,
115
- **dict(dict(line=dict(color=colour, width=1)), **kwargs)
115
+ **dict(line=dict(color=colour, width=1) | ({} if line is None else line), **kwargs)
116
116
  )
117
117
  tr['showlegend'] = False
118
118
  return tr
File without changes
File without changes