flightplotting 0.2.12__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 (23) hide show
  1. {flightplotting-0.2.12 → flightplotting-0.2.14}/PKG-INFO +4 -3
  2. {flightplotting-0.2.12 → flightplotting-0.2.14}/pyproject.toml +2 -2
  3. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/__init__.py +1 -0
  4. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/plots.py +186 -43
  5. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/templates.py +6 -14
  6. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/traces.py +22 -12
  7. flightplotting-0.2.14/uv.lock +345 -0
  8. flightplotting-0.2.12/uv.lock +0 -476
  9. {flightplotting-0.2.12 → flightplotting-0.2.14}/.github/workflows/publish_pypi.yml +0 -0
  10. {flightplotting-0.2.12 → flightplotting-0.2.14}/.gitignore +0 -0
  11. {flightplotting-0.2.12 → flightplotting-0.2.14}/.vscode/settings.json +0 -0
  12. {flightplotting-0.2.12 → flightplotting-0.2.14}/COPYING +0 -0
  13. {flightplotting-0.2.12 → flightplotting-0.2.14}/MANIFEST.in +0 -0
  14. {flightplotting-0.2.12 → flightplotting-0.2.14}/README.md +0 -0
  15. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/data/ColdDraftF3APlane.obj +0 -0
  16. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/data/__init__.py +0 -0
  17. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/model.py +0 -0
  18. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/py.typed +0 -0
  19. {flightplotting-0.2.12 → flightplotting-0.2.14}/src/plotting/titlerenderer.py +0 -0
  20. {flightplotting-0.2.12 → flightplotting-0.2.14}/tests/__init__.py +0 -0
  21. {flightplotting-0.2.12 → flightplotting-0.2.14}/tests/data/__init__.py +0 -0
  22. {flightplotting-0.2.12 → flightplotting-0.2.14}/tests/data/p23_flight.json +0 -0
  23. {flightplotting-0.2.12 → flightplotting-0.2.14}/tests/test_plots.py +0 -0
@@ -1,10 +1,11 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: flightplotting
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: Add your description here
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
+ License-File: COPYING
6
7
  Requires-Python: >=3.12
7
- Requires-Dist: flightanalysis>=0.3.14
8
+ Requires-Dist: flightdata>=0.3.5
8
9
  Requires-Dist: numpy>=2.1.3
9
10
  Requires-Dist: pandas>=2.2.3
10
11
  Requires-Dist: plotly>=5.24.1
@@ -1,15 +1,15 @@
1
1
  [project]
2
2
  name = "flightplotting"
3
- version="v0.2.12"
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" }]
7
7
  requires-python = ">=3.12"
8
8
  dependencies = [
9
- "flightanalysis>=0.3.14",
10
9
  "numpy>=2.1.3",
11
10
  "pandas>=2.2.3",
12
11
  "plotly>=5.24.1",
12
+ "flightdata>=0.3.5",
13
13
  ]
14
14
 
15
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,16 +16,18 @@ from plotting.traces import (
15
16
  )
16
17
 
17
18
  from flightdata import State
18
- from flightdata.base.labeling import get_appended_id
19
- from geometry import Coord
19
+ import geometry as g
20
20
  from plotting.model import obj
21
21
  import numpy.typing as npt
22
22
  import numpy as np
23
23
  import pandas as pd
24
- from typing import List, Union
25
- from flightanalysis.scoring.box import Box
24
+ from typing import List, Union, Callable, Literal
26
25
 
27
26
 
27
+
28
+ def get_colour(i):
29
+ return px.colors.qualitative.Plotly[i % len(px.colors.qualitative.Plotly)]
30
+
28
31
  def plotsec(
29
32
  secs: State | list[State] | dict[str, State],
30
33
  scale=5,
@@ -37,7 +40,10 @@ def plotsec(
37
40
  show_axes=False,
38
41
  ribb: bool = False,
39
42
  tips: bool = True,
43
+ ribbonhover="t",
40
44
  origin=False,
45
+ line=None,
46
+ modelscale=1,
41
47
  ):
42
48
  traces = []
43
49
  keys = None
@@ -51,22 +57,32 @@ def plotsec(
51
57
  else:
52
58
  keys = list(range(len(secs)))
53
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)
54
68
 
55
69
  for i, sec in enumerate(secs):
56
- text = sec.data.t #- sec.data.t.iloc[0]
57
- _color = color if color is not None else px.colors.qualitative.Plotly[i]
70
+ text = sec.data.t # - sec.data.t.iloc[0]
71
+
58
72
  if ribb:
59
- traces += ribbon(sec, scale * 1.85, _color, name=keys[i])
73
+ traces += ribbon(sec, 0.5 * scale * 1.85, _get_colour(i), name=keys[i], opacity=0.5, hover=ribbonhover)
60
74
  if tips:
61
- 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))
62
76
  if nmodels > 0:
63
- traces += meshes(nmodels, sec, _color, scale)
77
+ traces += meshes(nmodels, sec, _get_colour(i), scale * modelscale)
64
78
  if cg:
65
- traces.append(cgtrace(sec, line=dict(color=_color, width=2), name=keys[i], text=text))
79
+ traces.append(
80
+ cgtrace(sec, line=dict(color=_get_colour(i), width=2) | ({} if line is None else line), name=keys[i], text=text)
81
+ )
66
82
 
67
83
  if origin:
68
- traces += axestrace(Coord.zero(), 50)
69
-
84
+ traces += axestrace(g.Coord.zero(), 50)
85
+
70
86
  if showkeys:
71
87
  for i, key in enumerate(keys):
72
88
  traces.append(
@@ -75,7 +91,7 @@ def plotsec(
75
91
  y=[],
76
92
  z=[],
77
93
  mode="markers",
78
- marker=dict(size=5, color=px.colors.qualitative.Plotly[i]),
94
+ marker=dict(size=5, color=_get_colour(i)),
79
95
  name=key,
80
96
  showlegend=True,
81
97
  )
@@ -84,7 +100,7 @@ def plotsec(
84
100
  if fig is None:
85
101
  fig = go.Figure(
86
102
  data=traces,
87
- layout=go.Layout(template="flight3d+judge_view", uirevision="foo"),
103
+ layout=go.Layout(template="flight3d", uirevision="foo"),
88
104
  )
89
105
  if show_axes:
90
106
  fig.update_layout(
@@ -136,47 +152,33 @@ def plotdtw(sec: State, manoeuvres: List[str], span=3, fig=None):
136
152
 
137
153
 
138
154
  def plot_regions(
139
- st: State, lab_cols: list[str], span=3, colours=None, fig=None, box:Box=None, **kwargs
155
+ st: State,
156
+ label_group_name: str,
157
+ span=3,
158
+ colours=None,
159
+ fig=None,
160
+ ribbonhover="t",
161
+ **kwargs,
140
162
  ):
141
163
  colours = px.colors.qualitative.Plotly if colours is None else colours
142
- lab_cols = [lab_cols] if isinstance(lab_cols, str) else lab_cols
143
-
144
- st = st.label(clabs=st.cumulative_labels(*lab_cols))
145
-
146
- colmap = {}
147
164
 
148
165
  traces = []
149
- for i, (k, seg) in enumerate(st.split_labels("clabs").items()):
166
+ for i, k in enumerate(st.labels[label_group_name].keys()):
167
+ seg = getattr(st, label_group_name)[k]
150
168
  if len(seg) < 3:
151
169
  continue
152
- blab, id = get_appended_id(k)
153
- if blab not in colmap:
154
- colmap[blab] = colours[len(colmap) % len(colours)]
155
170
  traces += ribbon(
156
171
  seg,
157
172
  span,
158
- colmap[blab],
159
- name=blab,
160
- showlegend=int(id) == 0,
161
- **kwargs[blab] if blab in kwargs else {},
162
- )
163
- traces.append(
164
- go.Scatter3d(
165
- x=seg.pos.x,
166
- y=seg.pos.y,
167
- z=seg.pos.z,
168
- mode="lines",
169
- line=dict(width=0, color=colmap[blab]),
170
- name=k,
171
- showlegend=False,
172
- )
173
+ colours[i%len(colours)],
174
+ name=k,
175
+ hover=ribbonhover
173
176
  )
174
177
 
178
+
175
179
  if fig is None:
176
180
  fig = go.Figure(layout=go.Layout(template="flight3d+judge_view"))
177
181
  fig.add_traces(traces)
178
- if box:
179
- fig.add_traces(box.plot())
180
182
  return fig
181
183
 
182
184
 
@@ -316,7 +318,7 @@ def multi_y_subplots(data: dict[str, pd.DataFrame], x: npt.NDArray = None):
316
318
  cols=1,
317
319
  shared_xaxes=True,
318
320
  vertical_spacing=0.01,
319
- #subplot_titles=list(data.keys()),
321
+ # subplot_titles=list(data.keys()),
320
322
  )
321
323
 
322
324
  for row, (k, v) in enumerate(data.items(), 1):
@@ -352,7 +354,7 @@ def multi_y_subplots(data: dict[str, pd.DataFrame], x: npt.NDArray = None):
352
354
  yanchor="top",
353
355
  ),
354
356
  f"yaxis{i}": dict(
355
- title=list(data.keys())[i-1],
357
+ title=list(data.keys())[i - 1],
356
358
  showline=True,
357
359
  ),
358
360
  }
@@ -372,3 +374,144 @@ axis = dict(
372
374
  zerolinecolor="lightgrey",
373
375
  showline=True,
374
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
@@ -2,21 +2,14 @@
2
2
  import plotly.graph_objects as go
3
3
  import plotly.io as pio
4
4
 
5
- flight3d_template = go.layout.Template(layout=go.Layout(
5
+
6
+ generic3d_template = go.layout.Template(layout=go.Layout(
6
7
  margin=dict(l=0, r=0, t=0, b=0),
7
- scene=dict(
8
- aspectmode='data',
9
- ),
10
- legend=dict(
11
- font=dict(size=20),
12
- yanchor="top",
13
- y=0.99,
14
- xanchor="left",
15
- x=0.01
16
- )
8
+ scene=dict(aspectmode='data')
17
9
  ))
18
10
 
19
- pio.templates["flight3d"] = flight3d_template
11
+ pio.templates["generic3d"] = generic3d_template
12
+ pio.templates["flight3d"] = generic3d_template
20
13
 
21
14
  judges_view_template = go.layout.Template(layout=go.Layout(scene_camera=dict(
22
15
  up=dict(x=0, y=0, z=1),
@@ -32,10 +25,9 @@ pio.templates["judge_view"] = judges_view_template
32
25
  clean_paper_template = go.layout.Template(layout=go.Layout(
33
26
  margin=dict(l=0, r=0, t=0, b=0),
34
27
  scene=dict(
35
- aspectmode='data',
36
28
  xaxis = dict(visible=False),
37
29
  yaxis = dict(visible=False),
38
- zaxis =dict(visible=False)
30
+ zaxis = dict(visible=False)
39
31
  ),
40
32
  legend=dict(
41
33
  font=dict(size=20),
@@ -1,3 +1,4 @@
1
+ from typing import Literal
1
2
  import plotly.graph_objects as go
2
3
  import plotting.templates
3
4
  from geometry import Point, Coord, Transformation
@@ -23,7 +24,7 @@ def boxtrace():
23
24
  )]
24
25
 
25
26
 
26
- def meshes(npoints, seq: State, colour, scale=1, _obj: OBJ=None):
27
+ def meshes(npoints, seq: State | Transformation, colour: str=None, scale=1, _obj: OBJ=None):
27
28
  _obj = obj if _obj is None else _obj
28
29
  if scale != 1:
29
30
  _obj = _obj.scale(scale)
@@ -36,10 +37,10 @@ def meshes(npoints, seq: State, colour, scale=1, _obj: OBJ=None):
36
37
  locs = locs + list(np.cumsum(np.full(npoints-2, len(seq) / (npoints-1))).astype(int))
37
38
 
38
39
  ms = []
39
- for loc in locs:
40
+ for i, loc in enumerate(locs):
40
41
  ms.append(_obj.transform(
41
- Transformation(seq.pos[loc], seq.att[loc])
42
- ).create_mesh(colour,f"{seq.time.t[loc]:.1f}"))
42
+ seq.iloc[loc].transform if isinstance(seq, State) else seq[loc]
43
+ ).create_mesh(colour or "grey",f"{(seq.time.t[loc] if isinstance(seq, State) else i):.1f}"))
43
44
  return ms
44
45
 
45
46
  def vector(origin, direction, **kwargs):
@@ -106,12 +107,12 @@ def elementtraces(manoeuvre, sec: State):
106
107
 
107
108
 
108
109
 
109
- def tiptrace(seq, span, **kwargs):
110
+ def tiptrace(seq, span, line=None, **kwargs):
110
111
 
111
112
  def make_offset_trace(pos, colour):
112
113
  tr = trace3d(
113
114
  *seq.body_to_world(pos).data.T,
114
- **dict(dict(line=dict(color=colour, width=1)), **kwargs)
115
+ **dict(line=dict(color=colour, width=1) | ({} if line is None else line), **kwargs)
115
116
  )
116
117
  tr['showlegend'] = False
117
118
  return tr
@@ -201,24 +202,25 @@ def aoa_trace(sec, dash="dash", colours = px.colors.qualitative.Plotly):
201
202
  #sec = sec.append_columns(sec.aoa())
202
203
  return sec_col_trace(sec, ["alpha", "beta"], dash, colours, np.degrees)
203
204
 
204
- def axestrace(cid: Coord, length:float=20.0):
205
+ def axestrace(cid: Coord | Transformation, length:float=20.0, **kwargs):
205
206
  ntraces = []
206
207
  colours = {"x":"red", "y":"blue", "z":"green"}
207
208
  for i, ci in enumerate(cid):
209
+ if isinstance(ci, Transformation):
210
+ ci = ci.apply(Coord.zero())
208
211
  for ax, col in zip([ci.x_axis, ci.y_axis, ci.z_axis], list("xyz")):
209
212
  axis = Point.concatenate([ci.origin, ci.origin + ax * length])
210
213
  ntraces.append(go.Scatter3d(
211
214
  x=axis.x, y=axis.y, z=axis.z, mode="lines",
212
215
  line=dict(color=colours[col]),
213
- name=col,
214
- showlegend=True if i==0 else False
216
+ name=f"{i}_{col}",
217
+ **kwargs
215
218
  ))
216
219
 
217
220
  return ntraces
218
221
 
219
222
 
220
223
 
221
-
222
224
  def _npinterzip(a, b):
223
225
  """
224
226
  takes two numpy arrays and zips them.
@@ -243,7 +245,7 @@ def _npinterzip(a, b):
243
245
  return c
244
246
 
245
247
 
246
- def ribbon(sec: State, span: float, color, **kwargs):
248
+ def ribbon(sec: State, span: float, color, hover: Literal["i", "t"]='i', **kwargs):
247
249
  """TODO make the colouring more generic
248
250
  """
249
251
 
@@ -252,7 +254,13 @@ def ribbon(sec: State, span: float, color, **kwargs):
252
254
 
253
255
  points = Point(_npinterzip(left.data, right.data))
254
256
 
255
-
257
+ match hover:
258
+ case "i":
259
+ text=[f"{i}" for i in np.arange(len(sec)*2)]
260
+ case _:
261
+ text=[f"{t:.1f}" for t in _npinterzip(sec.t, sec.t)]
262
+
263
+
256
264
  _i = np.array(range(len(points) - 2)) # 1 2 3 4 5
257
265
 
258
266
  _js = np.array(range(1, len(points), 2))
@@ -265,5 +273,7 @@ def ribbon(sec: State, span: float, color, **kwargs):
265
273
  x=points.x, y=points.y, z=points.z, i=_i, j=_j, k=_k,
266
274
  intensitymode="cell",
267
275
  facecolor=np.full(len(_i), color),
276
+ text=text,
277
+ hovertemplate='i:%{text}<br>',
268
278
  **kwargs
269
279
  )]