knit-graphs 0.0.10__py3-none-any.whl → 0.0.11__py3-none-any.whl

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.
@@ -3,11 +3,13 @@
3
3
  This module provides comprehensive visualization capabilities for knit graphs using Plotly.
4
4
  It handles the positioning of loops, rendering of yarn paths, stitch edges, and cable crossings to create interactive 2D visualizations of knitted structures.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
8
9
  import os
9
10
  import sys
10
- from typing import Iterable, cast
11
+ from collections.abc import Iterable
12
+ from typing import TypedDict, cast
11
13
 
12
14
  import plotly.io as pio
13
15
  from networkx import DiGraph
@@ -20,14 +22,22 @@ from knit_graphs.Loop import Loop
20
22
  from knit_graphs.Pull_Direction import Pull_Direction
21
23
 
22
24
 
25
+ class TraceData(TypedDict):
26
+ """Typing specification for the dictionaries passes as traces to Plotly"""
27
+
28
+ x: list[float | None]
29
+ y: list[float | None]
30
+ edge: list[tuple[Loop, Loop]]
31
+ is_start: list[bool]
32
+
33
+
23
34
  def configure_plotly_environment() -> None:
24
35
  """Auto-configure Plotly based on environment detection to avoid socket issues."""
25
36
  # Check if we're in a testing environment
26
- if ('pytest' in sys.modules or 'unittest' in sys.modules or
27
- os.environ.get('TESTING') or os.environ.get('CI')):
37
+ if "pytest" in sys.modules or "unittest" in sys.modules or os.environ.get("TESTING") or os.environ.get("CI"):
28
38
  # For testing, don't set a default renderer - we'll handle this with show_figure=False
29
39
  pass
30
- elif 'ipykernel' in sys.modules or 'jupyter' in sys.modules:
40
+ elif "ipykernel" in sys.modules or "jupyter" in sys.modules:
31
41
  # Jupyter environment - use notebook renderer
32
42
  pio.renderers.default = "notebook"
33
43
  else:
@@ -59,10 +69,15 @@ class Knit_Graph_Visualizer:
59
69
  first_course_index (int): The index of the first (bottom) course to visualize.
60
70
  """
61
71
 
62
- def __init__(self, knit_graph: Knit_Graph, first_course_index: int = 0, top_course_index: int | None = None,
63
- start_on_left: bool = True,
64
- balance_by_base_width: bool = False,
65
- left_zero_align: bool = True):
72
+ def __init__(
73
+ self,
74
+ knit_graph: Knit_Graph,
75
+ first_course_index: int = 0,
76
+ top_course_index: int | None = None,
77
+ start_on_left: bool = True,
78
+ balance_by_base_width: bool = False,
79
+ left_zero_align: bool = True,
80
+ ):
66
81
  """Initialize the knit graph visualizer with specified configuration options.
67
82
 
68
83
  Args:
@@ -91,12 +106,42 @@ class Knit_Graph_Visualizer:
91
106
  self._loops_need_placement: set[Loop] = set()
92
107
  self._loop_markers: list[Scatter] = []
93
108
  self._yarn_traces: list[Scatter] = []
94
- self._top_knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
95
- self._bot_knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
96
- self._top_purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
97
- self._bot_purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
98
- self._knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
99
- self._purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
109
+ self._top_knit_trace_data: TraceData = {
110
+ "x": [],
111
+ "y": [],
112
+ "edge": [],
113
+ "is_start": [],
114
+ }
115
+ self._bot_knit_trace_data: TraceData = {
116
+ "x": [],
117
+ "y": [],
118
+ "edge": [],
119
+ "is_start": [],
120
+ }
121
+ self._top_purl_trace_data: TraceData = {
122
+ "x": [],
123
+ "y": [],
124
+ "edge": [],
125
+ "is_start": [],
126
+ }
127
+ self._bot_purl_trace_data: TraceData = {
128
+ "x": [],
129
+ "y": [],
130
+ "edge": [],
131
+ "is_start": [],
132
+ }
133
+ self._knit_trace_data: TraceData = {
134
+ "x": [],
135
+ "y": [],
136
+ "edge": [],
137
+ "is_start": [],
138
+ }
139
+ self._purl_trace_data: TraceData = {
140
+ "x": [],
141
+ "y": [],
142
+ "edge": [],
143
+ "is_start": [],
144
+ }
100
145
  # Form the visualization.
101
146
  self._position_loops()
102
147
  self._set_loop_markers()
@@ -112,18 +157,23 @@ class Knit_Graph_Visualizer:
112
157
  Returns:
113
158
  Figure: The plotly figure object.
114
159
  """
115
- go_layout = Layout(title=graph_title,
116
- showlegend=True,
117
- hovermode='closest',
118
- margin=dict(b=20, l=5, r=5, t=40)
119
- )
120
- figure_data = [self._top_knit_trace(), self._top_purl_trace(),
121
- self._no_cross_knit_trace(), self._no_cross_purl_trace(),
122
- self._bot_knit_trace(), self._bot_purl_trace()]
160
+ go_layout = Layout(
161
+ title=graph_title,
162
+ showlegend=True,
163
+ hovermode="closest",
164
+ margin={"b": 20, "l": 5, "r": 5, "t": 40},
165
+ )
166
+ figure_data = [
167
+ self._top_knit_trace(),
168
+ self._top_purl_trace(),
169
+ self._no_cross_knit_trace(),
170
+ self._no_cross_purl_trace(),
171
+ self._bot_knit_trace(),
172
+ self._bot_purl_trace(),
173
+ ]
123
174
  figure_data.extend(self._yarn_traces)
124
175
  figure_data.extend(self._loop_markers)
125
- fig = Figure(data=figure_data,
126
- layout=go_layout)
176
+ fig = Figure(data=figure_data, layout=go_layout)
127
177
  return fig
128
178
 
129
179
  def show_figure(self, graph_title: str = "Knit Graph", renderer: str | None = None) -> None:
@@ -137,11 +187,11 @@ class Knit_Graph_Visualizer:
137
187
 
138
188
  # Configure display to minimize resource usage
139
189
  config = {
140
- 'displayModeBar': False, # Hide toolbar to reduce resource usage
141
- 'displaylogo': False, # Hide plotly logo
142
- 'staticPlot': False, # Keep interactive
143
- 'scrollZoom': False, # Allow zoom
144
- 'doubleClick': 'reset+autosize' # Double-click behavior
190
+ "displayModeBar": False, # Hide toolbar to reduce resource usage
191
+ "displaylogo": False, # Hide plotly logo
192
+ "staticPlot": False, # Keep interactive
193
+ "scrollZoom": False, # Allow zoom
194
+ "doubleClick": "reset+autosize", # Double-click behavior
145
195
  }
146
196
 
147
197
  try:
@@ -151,9 +201,11 @@ class Knit_Graph_Visualizer:
151
201
  fig.show(config=config)
152
202
  except Exception as e:
153
203
  print(f"Warning: Could not display figure: {e}")
154
- print("Figure created successfully but display failed. Consider using show_figure=False and accessing the returned Figure object directly.")
204
+ print(
205
+ "Figure created successfully but display failed. Consider using show_figure=False and accessing the returned Figure object directly."
206
+ )
155
207
 
156
- def _no_cross_knit_trace(self, line_width: float = 4.0, knit_color: str = 'blue') -> Scatter:
208
+ def _no_cross_knit_trace(self, line_width: float = 4.0, knit_color: str = "blue") -> Scatter:
157
209
  """Create a scatter trace for knit stitches not involved in cable crossings.
158
210
 
159
211
  Args:
@@ -165,7 +217,7 @@ class Knit_Graph_Visualizer:
165
217
  """
166
218
  return self._stitch_trace(self._knit_trace_data, "Knit Stitches", knit_color, line_width, opacity=0.8)
167
219
 
168
- def _top_knit_trace(self, line_width: float = 5.0, knit_color: str = 'blue') -> Scatter:
220
+ def _top_knit_trace(self, line_width: float = 5.0, knit_color: str = "blue") -> Scatter:
169
221
  """Create a scatter trace for knit stitches that cross over other stitches in cables.
170
222
 
171
223
  Args:
@@ -175,9 +227,15 @@ class Knit_Graph_Visualizer:
175
227
  Returns:
176
228
  Scatter: The plotly scatter object used to visualize knit stitches on top of cable crossings.
177
229
  """
178
- return self._stitch_trace(self._top_knit_trace_data, "Knit Stitches on Top of Cable", knit_color, line_width, opacity=1.0)
179
-
180
- def _bot_knit_trace(self, line_width: float = 3.0, knit_color: str = 'blue') -> Scatter:
230
+ return self._stitch_trace(
231
+ self._top_knit_trace_data,
232
+ "Knit Stitches on Top of Cable",
233
+ knit_color,
234
+ line_width,
235
+ opacity=1.0,
236
+ )
237
+
238
+ def _bot_knit_trace(self, line_width: float = 3.0, knit_color: str = "blue") -> Scatter:
181
239
  """Create a scatter trace for knit stitches that cross under other stitches in cables.
182
240
 
183
241
  Args:
@@ -187,9 +245,15 @@ class Knit_Graph_Visualizer:
187
245
  Returns:
188
246
  Scatter: The plotly scatter object used to visualize knit stitches below cable crossings.
189
247
  """
190
- return self._stitch_trace(self._bot_knit_trace_data, "Knit Stitches Below Cable", knit_color, line_width, opacity=.5)
191
-
192
- def _no_cross_purl_trace(self, line_width: float = 4.0, purl_color: str = 'red') -> Scatter:
248
+ return self._stitch_trace(
249
+ self._bot_knit_trace_data,
250
+ "Knit Stitches Below Cable",
251
+ knit_color,
252
+ line_width,
253
+ opacity=0.5,
254
+ )
255
+
256
+ def _no_cross_purl_trace(self, line_width: float = 4.0, purl_color: str = "red") -> Scatter:
193
257
  """Create a scatter trace for purl stitches not involved in cable crossings.
194
258
 
195
259
  Args:
@@ -201,7 +265,7 @@ class Knit_Graph_Visualizer:
201
265
  """
202
266
  return self._stitch_trace(self._purl_trace_data, "Purl Stitches", purl_color, line_width, opacity=0.8)
203
267
 
204
- def _top_purl_trace(self, line_width: float = 5.0, purl_color: str = 'red') -> Scatter:
268
+ def _top_purl_trace(self, line_width: float = 5.0, purl_color: str = "red") -> Scatter:
205
269
  """Create a scatter trace for purl stitches that cross over other stitches in cables.
206
270
 
207
271
  Args:
@@ -211,9 +275,15 @@ class Knit_Graph_Visualizer:
211
275
  Returns:
212
276
  Scatter: The plotly scatter object used to visualize purl stitches on top of cable crossings.
213
277
  """
214
- return self._stitch_trace(self._top_purl_trace_data, "Purl Stitches on Top of Cable", purl_color, line_width, opacity=1.0)
215
-
216
- def _bot_purl_trace(self, line_width: float = 3.0, purl_color: str = 'red') -> Scatter:
278
+ return self._stitch_trace(
279
+ self._top_purl_trace_data,
280
+ "Purl Stitches on Top of Cable",
281
+ purl_color,
282
+ line_width,
283
+ opacity=1.0,
284
+ )
285
+
286
+ def _bot_purl_trace(self, line_width: float = 3.0, purl_color: str = "red") -> Scatter:
217
287
  """Create a scatter trace for purl stitches that cross under other stitches in cables.
218
288
 
219
289
  Args:
@@ -223,14 +293,26 @@ class Knit_Graph_Visualizer:
223
293
  Returns:
224
294
  Scatter: The plotly scatter object used to visualize purl stitches below cable crossings.
225
295
  """
226
- return self._stitch_trace(self._bot_purl_trace_data, "Purl Stitches Below Cable", purl_color, line_width, opacity=.5)
296
+ return self._stitch_trace(
297
+ self._bot_purl_trace_data,
298
+ "Purl Stitches Below Cable",
299
+ purl_color,
300
+ line_width,
301
+ opacity=0.5,
302
+ )
227
303
 
228
304
  @staticmethod
229
- def _stitch_trace(trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]], trace_name: str, trace_color: str, line_width: float, opacity: float) -> Scatter:
305
+ def _stitch_trace(
306
+ trace_data: TraceData,
307
+ trace_name: str,
308
+ trace_color: str,
309
+ line_width: float,
310
+ opacity: float,
311
+ ) -> Scatter:
230
312
  """Create a generic scatter trace for stitch visualization with specified styling.
231
313
 
232
314
  Args:
233
- trace_data (dict): The trace data containing x, y coordinates and edge information to be plotted.
315
+ trace_data (dict[str, list[float | None] | list[tuple[Loop, Loop] | list[bool]]]): The trace data containing x, y coordinates and edge information to be plotted.
234
316
  trace_name (str): The name of the trace to show in the figure legend.
235
317
  trace_color (str): The color of the trace lines.
236
318
  line_width (float): The width of lines representing the stitch edges.
@@ -239,15 +321,21 @@ class Knit_Graph_Visualizer:
239
321
  Returns:
240
322
  Scatter: The plotly scatter object configured to visualize the given stitch traces.
241
323
  """
242
- return Scatter(name=trace_name,
243
- x=trace_data['x'], y=trace_data['y'],
244
- line=dict(width=line_width, color=trace_color, dash='solid'),
245
- opacity=opacity,
246
- mode='lines')
324
+ return Scatter(
325
+ name=trace_name,
326
+ x=trace_data["x"],
327
+ y=trace_data["y"],
328
+ line={"width": line_width, "color": trace_color, "dash": "solid"},
329
+ opacity=opacity,
330
+ mode="lines",
331
+ )
247
332
 
248
333
  def _add_cable_edges(self) -> None:
249
334
  """Add all stitch edges that are involved in cable crossings to the appropriate trace data."""
250
- for left_loop, right_loop in self.knit_graph.braid_graph.loop_crossing_graph.edges:
335
+ for (
336
+ left_loop,
337
+ right_loop,
338
+ ) in self.knit_graph.braid_graph.loop_crossing_graph.edges:
251
339
  crossing_direction = self.knit_graph.braid_graph.get_crossing(left_loop, right_loop)
252
340
  for left_parent in left_loop.parent_loops:
253
341
  self._add_stitch_edge(left_parent, left_loop, crossing_direction)
@@ -259,8 +347,11 @@ class Knit_Graph_Visualizer:
259
347
  self._add_cable_edges()
260
348
  # Add remaining stitches as though they have no cable crossing.
261
349
  for u, v in self.knit_graph.stitch_graph.edges:
262
- if (not self._stitch_has_position(u, v) # This edge has not been placed
263
- and self._loop_has_position(u) and self._loop_has_position(v)): # Both loops do have positions.
350
+ if (
351
+ not self._stitch_has_position(u, v) # This edge has not been placed
352
+ and self._loop_has_position(u)
353
+ and self._loop_has_position(v)
354
+ ): # Both loops do have positions.
264
355
  self._add_stitch_edge(u, v, Crossing_Direction.No_Cross)
265
356
 
266
357
  def _add_stitch_edge(self, u: Loop, v: Loop, crossing_direction: Crossing_Direction) -> None:
@@ -283,53 +374,70 @@ class Knit_Graph_Visualizer:
283
374
  else:
284
375
  trace_data = self._purl_trace_data
285
376
  self.data_graph.add_edge(u, v, pull_direction=pull_direction)
286
- trace_data['x'].append(self._get_x_of_loop(u))
287
- trace_data['y'].append(self._get_y_of_loop(u))
288
- trace_data['edge'].append((u, v))
289
- trace_data['is_start'].append(True)
290
- trace_data['x'].append(self._get_x_of_loop(v))
291
- trace_data['y'].append(self._get_y_of_loop(v))
292
- trace_data['edge'].append((u, v))
293
- trace_data['is_start'].append(False)
294
- trace_data['x'].append(None)
295
- trace_data['y'].append(None)
377
+ trace_data["x"].append(self._get_x_of_loop(u))
378
+ trace_data["y"].append(self._get_y_of_loop(u))
379
+ trace_data["edge"].append((u, v))
380
+ trace_data["is_start"].append(True)
381
+ trace_data["x"].append(self._get_x_of_loop(v))
382
+ trace_data["y"].append(self._get_y_of_loop(v))
383
+ trace_data["edge"].append((u, v))
384
+ trace_data["is_start"].append(False)
385
+ trace_data["x"].append(None)
386
+ trace_data["y"].append(None)
296
387
 
297
388
  def _set_loop_markers(self, loop_size: float = 30.0, loop_border_width: float = 2.0) -> None:
298
389
  """Create plotly scatter objects to mark the position of each loop in the visualization."""
299
- yarns_to_loop_data = {yarn: {'x': [self._get_x_of_loop(loop) for loop in yarn],
300
- 'y': [self._get_y_of_loop(loop) for loop in yarn],
301
- 'loop_id': [loop.loop_id for loop in yarn]
302
- }
303
- for yarn in self.knit_graph.yarns
304
- }
305
- self._loop_markers = [Scatter(name=f"Loops on {yarn.yarn_id}", x=yarn_data['x'], y=yarn_data['y'], text=yarn_data['loop_id'],
306
- textposition='middle center',
307
- mode='markers+text',
308
- marker=dict(
309
- reversescale=True,
310
- color=yarn.properties.color,
311
- size=loop_size,
312
- line_width=loop_border_width))
313
- for yarn, yarn_data in yarns_to_loop_data.items()]
390
+ yarns_to_loop_data = {
391
+ yarn: {
392
+ "x": [self._get_x_of_loop(loop) for loop in yarn],
393
+ "y": [self._get_y_of_loop(loop) for loop in yarn],
394
+ "loop_id": [loop.loop_id for loop in yarn],
395
+ }
396
+ for yarn in self.knit_graph.yarns
397
+ }
398
+ self._loop_markers = [
399
+ Scatter(
400
+ name=f"Loops on {yarn.yarn_id}",
401
+ x=yarn_data["x"],
402
+ y=yarn_data["y"],
403
+ text=yarn_data["loop_id"],
404
+ textposition="middle center",
405
+ mode="markers+text",
406
+ marker={
407
+ "reversescale": True,
408
+ "color": yarn.properties.color,
409
+ "size": loop_size,
410
+ "line_width": loop_border_width,
411
+ },
412
+ )
413
+ for yarn, yarn_data in yarns_to_loop_data.items()
414
+ ]
314
415
 
315
416
  def _set_yarn_traces(self, line_width: float = 1.0, smoothing: float = 1.3) -> None:
316
417
  """Create plotly traces representing the path of each yarn through the knitted structure."""
317
418
  yarns_to_float_data = {}
318
419
  for yarn in self.knit_graph.yarns:
319
- float_data: dict[str, list[float]] = {'x': [], 'y': []}
420
+ float_data: dict[str, list[float]] = {"x": [], "y": []}
320
421
  for u in yarn:
321
422
  if self._loop_has_position(u):
322
- float_data['x'].append(self._get_x_of_loop(u))
323
- float_data['y'].append(self._get_y_of_loop(u))
423
+ float_data["x"].append(self._get_x_of_loop(u))
424
+ float_data["y"].append(self._get_y_of_loop(u))
324
425
  yarns_to_float_data[yarn] = float_data
325
- self._yarn_traces = [Scatter(name=yarn.yarn_id,
326
- x=float_data['x'], y=float_data['y'],
327
- line=dict(width=line_width,
328
- color=yarn.properties.color,
329
- shape='spline',
330
- smoothing=smoothing),
331
- mode='lines')
332
- for yarn, float_data in yarns_to_float_data.items()]
426
+ self._yarn_traces = [
427
+ Scatter(
428
+ name=yarn.yarn_id,
429
+ x=float_data["x"],
430
+ y=float_data["y"],
431
+ line={
432
+ "width": line_width,
433
+ "color": yarn.properties.color,
434
+ "shape": "spline",
435
+ "smoothing": smoothing,
436
+ },
437
+ mode="lines",
438
+ )
439
+ for yarn, float_data in yarns_to_float_data.items()
440
+ ]
333
441
 
334
442
  def _position_loops(self) -> None:
335
443
  """Calculate and set the x,y coordinate positions of all loops to be visualized."""
@@ -340,8 +448,14 @@ class Knit_Graph_Visualizer:
340
448
 
341
449
  def _shift_knit_purl(self, shift: float = 0.1) -> None:
342
450
  """Adjust the horizontal position of loops to visually distinguish knit from purl stitches."""
343
- has_knits = any(self.knit_graph.get_pull_direction(u, v) is Pull_Direction.BtF for u, v in self.knit_graph.stitch_graph.edges)
344
- has_purls = any(self.knit_graph.get_pull_direction(u, v) is Pull_Direction.FtB for u, v in self.knit_graph.stitch_graph.edges)
451
+ has_knits = any(
452
+ self.knit_graph.get_pull_direction(u, v) is Pull_Direction.BtF
453
+ for u, v in self.knit_graph.stitch_graph.edges
454
+ )
455
+ has_purls = any(
456
+ self.knit_graph.get_pull_direction(u, v) is Pull_Direction.FtB
457
+ for u, v in self.knit_graph.stitch_graph.edges
458
+ )
345
459
  if not (has_knits and has_purls):
346
460
  return # Don't make any changes, because all stitches are of the same type.
347
461
  yarn_over_align = set()
@@ -350,8 +464,12 @@ class Knit_Graph_Visualizer:
350
464
  if self.knit_graph.has_child_loop(loop): # Align yarn-overs with one child to its child
351
465
  yarn_over_align.add(loop)
352
466
  continue # Don't shift yarn-overs
353
- knit_parents = len([u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.BtF])
354
- purl_parents = len([u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.FtB])
467
+ knit_parents = len(
468
+ [u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.BtF]
469
+ )
470
+ purl_parents = len(
471
+ [u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.FtB]
472
+ )
355
473
  if knit_parents > purl_parents: # Shift the loop as though it is being knit.
356
474
  self._set_x_of_loop(loop, self._get_x_of_loop(loop) - shift)
357
475
  elif purl_parents > knit_parents: # Shift the loop as though it is being purled.
@@ -367,12 +485,21 @@ class Knit_Graph_Visualizer:
367
485
  for yarn in self.knit_graph.yarns:
368
486
  for u, v, front_loops in yarn.loops_in_front_of_floats():
369
487
  for front_loop in front_loops:
370
- if u in self._get_course_of_loop(front_loop) and v in self._get_course_of_loop(front_loop): # same course, adjust float position
371
- self._set_y_of_loop(front_loop, self._get_y_of_loop(front_loop) - float_increment) # shift loop down to show it is in front of the float.
488
+ if u in self._get_course_of_loop(front_loop) and v in self._get_course_of_loop(
489
+ front_loop
490
+ ): # same course, adjust float position
491
+ self._set_y_of_loop(
492
+ front_loop,
493
+ self._get_y_of_loop(front_loop) - float_increment,
494
+ ) # shift loop down to show it is in front of the float.
372
495
  for u, v, back_loops in yarn.loops_behind_floats():
373
496
  for back_loop in back_loops:
374
- if u in self._get_course_of_loop(back_loop) and v in self._get_course_of_loop(back_loop): # same course, adjust float position
375
- self._set_y_of_loop(back_loop, self._get_y_of_loop(back_loop) + float_increment) # shift loop up to show it is behind the float.
497
+ if u in self._get_course_of_loop(back_loop) and v in self._get_course_of_loop(
498
+ back_loop
499
+ ): # same course, adjust float position
500
+ self._set_y_of_loop(
501
+ back_loop, self._get_y_of_loop(back_loop) + float_increment
502
+ ) # shift loop up to show it is behind the float.
376
503
 
377
504
  def _get_course_of_loop(self, loop: Loop) -> Course:
378
505
  """Get the course (horizontal row) that contains the specified loop."""
@@ -389,28 +516,28 @@ class Knit_Graph_Visualizer:
389
516
  def _set_x_of_loop(self, loop: Loop, x: float) -> None:
390
517
  """Update the x coordinate of a loop that already exists in the visualization data graph."""
391
518
  if self._loop_has_position(loop):
392
- self.data_graph.nodes[loop]['x'] = x
519
+ self.data_graph.nodes[loop]["x"] = x
393
520
  else:
394
521
  raise KeyError(f"Loop {loop} is not in the data graph")
395
522
 
396
523
  def _set_y_of_loop(self, loop: Loop, y: float) -> None:
397
524
  """Update the y coordinate of a loop that already exists in the visualization data graph."""
398
525
  if self._loop_has_position(loop):
399
- self.data_graph.nodes[loop]['y'] = y
526
+ self.data_graph.nodes[loop]["y"] = y
400
527
  else:
401
528
  raise KeyError(f"Loop {loop} is not in the data graph")
402
529
 
403
530
  def _get_x_of_loop(self, loop: Loop) -> float:
404
531
  """Get the x coordinate of a loop from the visualization data graph."""
405
532
  if self._loop_has_position(loop):
406
- return float(self.data_graph.nodes[loop]['x'])
533
+ return float(self.data_graph.nodes[loop]["x"])
407
534
  else:
408
535
  raise KeyError(f"Loop {loop} is not in the data graph")
409
536
 
410
537
  def _get_y_of_loop(self, loop: Loop) -> float:
411
538
  """Get the y coordinate of a loop from the visualization data graph."""
412
539
  if self._loop_has_position(loop):
413
- return float(self.data_graph.nodes[loop]['y'])
540
+ return float(self.data_graph.nodes[loop]["y"])
414
541
  else:
415
542
  raise KeyError(f"Loop {loop} is not in the data graph")
416
543
 
@@ -425,7 +552,7 @@ class Knit_Graph_Visualizer:
425
552
  def _place_loops_in_courses(self, course_spacing: float = 1.0) -> None:
426
553
  """Position loops in all courses above the base course using parent relationships and yarn connections."""
427
554
  y = course_spacing
428
- for course in self.courses[self.first_course_index + 1:self.top_course_index]:
555
+ for course in self.courses[self.first_course_index + 1 : self.top_course_index]:
429
556
  self._place_loops_by_parents(course, y)
430
557
  self._swap_loops_in_cables(course)
431
558
  self._left_align_course(course)
@@ -437,14 +564,16 @@ class Knit_Graph_Visualizer:
437
564
  for left_loop in course:
438
565
  for right_loop in self.knit_graph.braid_graph.left_crossing_loops(left_loop):
439
566
  crossing_direction = self.knit_graph.braid_graph.get_crossing(left_loop, right_loop)
440
- if crossing_direction is not Crossing_Direction.No_Cross: # Swap the position of loops that cross each other.
567
+ if (
568
+ crossing_direction is not Crossing_Direction.No_Cross
569
+ ): # Swap the position of loops that cross each other.
441
570
  left_x = self._get_x_of_loop(left_loop)
442
571
  self._set_x_of_loop(left_loop, self._get_x_of_loop(right_loop))
443
572
  self._set_x_of_loop(right_loop, left_x)
444
573
 
445
574
  def _place_loops_by_parents(self, course: Course, y: float) -> None:
446
575
  """Position loops in a course based on the average position of their parent loops."""
447
- for x, loop in enumerate(course):
576
+ for _x, loop in enumerate(course):
448
577
  self._set_loop_x_by_parent_average(loop, y)
449
578
  placed_loops = set()
450
579
  for loop in self._loops_need_placement:
@@ -464,10 +593,14 @@ class Knit_Graph_Visualizer:
464
593
  def _parent_weight(stack_position: int) -> float:
465
594
  return float(len(loop.parent_loops) - stack_position)
466
595
 
467
- parent_positions = {self._get_x_of_loop(parent_loop) * _parent_weight(stack_pos): # position of parents weighted by their stack position.
468
- _parent_weight(stack_pos) # weight of the stack position.
469
- for stack_pos, parent_loop in enumerate(loop.parent_loops)
470
- if self.data_graph.has_node(parent_loop)} # Only include parent loops that are positioned.
596
+ parent_positions = {
597
+ self._get_x_of_loop(parent_loop)
598
+ * _parent_weight(stack_pos): _parent_weight( # position of parents weighted by their stack position.
599
+ stack_pos
600
+ ) # weight of the stack position.
601
+ for stack_pos, parent_loop in enumerate(loop.parent_loops)
602
+ if self.data_graph.has_node(parent_loop)
603
+ } # Only include parent loops that are positioned.
471
604
  x = sum(parent_positions.keys()) / sum(parent_positions.values())
472
605
  self._place_loop(loop, x=x, y=y)
473
606
 
@@ -478,26 +611,33 @@ class Knit_Graph_Visualizer:
478
611
  prior_loop = loop.prior_loop_on_yarn()
479
612
  next_loop = loop.next_loop_on_yarn()
480
613
  if prior_loop is not None and self._loop_has_position(prior_loop):
481
- if self._get_y_of_loop(prior_loop) == y: # Include the spacing to ensure these are not at overlapping positions.
614
+ if (
615
+ self._get_y_of_loop(prior_loop) == y
616
+ ): # Include the spacing to ensure these are not at overlapping positions.
482
617
  x_neighbors.append(self._get_x_of_loop(prior_loop) + spacing)
483
618
  else: # Don't include spacing because the prior loop is on the prior course.
484
619
  x_neighbors.append(self._get_x_of_loop(prior_loop))
485
620
  if next_loop is not None and self._loop_has_position(next_loop):
486
- if self._get_y_of_loop(next_loop) == y: # Include the spacing to ensure these are not at overlapping positions.
621
+ if (
622
+ self._get_y_of_loop(next_loop) == y
623
+ ): # Include the spacing to ensure these are not at overlapping positions.
487
624
  x_neighbors.append(self._get_x_of_loop(next_loop) - spacing)
488
625
  else: # Don't include spacing because the prior loop is on the prior course.
489
626
  x_neighbors.append(self._get_x_of_loop(next_loop))
490
627
  if len(x_neighbors) == 0:
491
628
  return False
492
- x = (sum(x_neighbors) / float(len(x_neighbors))) # the average of the two neighbors
629
+ x = sum(x_neighbors) / float(len(x_neighbors)) # the average of the two neighbors
493
630
  self._place_loop(loop, x=x, y=y)
494
631
  return True
495
632
 
496
633
  def _position_base_course(self) -> None:
497
634
  """Position the loops in the bottom course of the visualization and establish base metrics."""
498
635
  base_course = self.courses[self.first_course_index]
499
- if (len(self.courses) > self.first_course_index + 1 # There are more courses to show after the base course
500
- and base_course.in_round_with(self.courses[self.first_course_index + 1])): # The first course is knit in the round to form a tube structure.
636
+ if len(
637
+ self.courses
638
+ ) > self.first_course_index + 1 and base_course.in_round_with( # There are more courses to show after the base course
639
+ self.courses[self.first_course_index + 1]
640
+ ): # The first course is knit in the round to form a tube structure.
501
641
  self._get_base_round_course_positions(base_course)
502
642
  else:
503
643
  self._get_base_row_course_positions(base_course)
@@ -506,7 +646,9 @@ class Knit_Graph_Visualizer:
506
646
  max_x = max(self._get_x_of_loop(loop) for loop in base_course)
507
647
  self.base_width = max_x - self.base_left
508
648
 
509
- def _get_base_round_course_positions(self, base_course: Course, loop_space: float = 1.0, back_shift: float = 0.5) -> None:
649
+ def _get_base_round_course_positions(
650
+ self, base_course: Course, loop_space: float = 1.0, back_shift: float = 0.5
651
+ ) -> None:
510
652
  """Position loops in the base course for circular/tube knitting structure."""
511
653
  split_index = len(base_course) // 2 # Split the course in half to form a tube.
512
654
  front_loops: list[Loop] = cast(list[Loop], base_course[:split_index])
@@ -516,12 +658,20 @@ class Knit_Graph_Visualizer:
516
658
  back_loops = [*reversed(back_loops)]
517
659
  else:
518
660
  front_loops = [*reversed(front_loops)]
519
- for x, l in enumerate(front_loops):
520
- self._place_loop(l, x=x, y=0)
661
+ for x, front_loop in enumerate(front_loops):
662
+ self._place_loop(front_loop, x=x, y=0)
521
663
  for x, back_loop in enumerate(back_loops):
522
- float_positions = [self._get_x_of_loop(front_loop) for front_loop in back_loop.front_floats if front_loop in front_set]
523
- if len(float_positions) > 0: # If the back loop is floating behind other loops in the front of the course, set the position to be centered between the loops it is floating behind.
524
- self._place_loop(back_loop, x=sum(float_positions) / float(len(float_positions)), y=0.0)
664
+ float_positions = [
665
+ self._get_x_of_loop(front_loop) for front_loop in back_loop.front_floats if front_loop in front_set
666
+ ]
667
+ if (
668
+ len(float_positions) > 0
669
+ ): # If the back loop is floating behind other loops in the front of the course, set the position to be centered between the loops it is floating behind.
670
+ self._place_loop(
671
+ back_loop,
672
+ x=sum(float_positions) / float(len(float_positions)),
673
+ y=0.0,
674
+ )
525
675
  elif self.start_on_left:
526
676
  self._place_loop(back_loop, x=(x * loop_space) + back_shift, y=0)
527
677
  else:
@@ -549,6 +699,7 @@ class Knit_Graph_Visualizer:
549
699
  max_x = max(self._get_x_of_loop(loop) for loop in course)
550
700
  course_width = max_x - current_left
551
701
  if self.balance_by_base_width and course_width != self.base_width:
702
+
552
703
  def _target_distance_from_left(l: Loop) -> float:
553
704
  current_distance_from_left = self._get_x_of_loop(l) - current_left
554
705
  return (current_distance_from_left * self.base_width) / course_width
@@ -556,7 +707,7 @@ class Knit_Graph_Visualizer:
556
707
  for loop in course:
557
708
  self._set_x_of_loop(loop, _target_distance_from_left(loop) + current_left)
558
709
 
559
- def x_coordinate_differences(self, other: Knit_Graph_Visualizer) -> dict[Loop: tuple[float | None, float | None]]:
710
+ def x_coordinate_differences(self, other: Knit_Graph_Visualizer) -> dict[Loop, tuple[float | None, float | None]]:
560
711
  """Find the differences in x-coordinates between two knitgraph visualizations. Used for testing and comparing visualization results.
561
712
 
562
713
  Args:
@@ -570,12 +721,28 @@ class Knit_Graph_Visualizer:
570
721
  ** The second value of each tuple is the x-coordinate of the loop in the other visualization or NOne if the loop is not in that visualization.
571
722
 
572
723
  """
573
- differences = {l: (self._get_x_of_loop(l), None) for l in self.data_graph.nodes if not other.data_graph.has_node(l)}
574
- differences.update({l: (None, other._get_x_of_loop(l)) for l in other.data_graph.nodes if not self.data_graph.has_node(l)})
575
- differences.update({l: (self._get_x_of_loop(l), other._get_x_of_loop(l)) for l in self.data_graph.nodes if other.data_graph.has_node(l) and self._get_x_of_loop(l) != other._get_x_of_loop(l)})
724
+ differences: dict[Loop, tuple[float | None, float | None]] = {
725
+ cast(Loop, l): (self._get_x_of_loop(l), None)
726
+ for l in self.data_graph.nodes
727
+ if not other.data_graph.has_node(l)
728
+ }
729
+ differences.update(
730
+ {
731
+ cast(Loop, l): (None, other._get_x_of_loop(l))
732
+ for l in other.data_graph.nodes
733
+ if not self.data_graph.has_node(l)
734
+ }
735
+ )
736
+ differences.update(
737
+ {
738
+ cast(Loop, l): (self._get_x_of_loop(l), other._get_x_of_loop(l))
739
+ for l in self.data_graph.nodes
740
+ if other.data_graph.has_node(l) and self._get_x_of_loop(l) != other._get_x_of_loop(l)
741
+ }
742
+ )
576
743
  return differences
577
744
 
578
- def y_coordinate_differences(self, other: Knit_Graph_Visualizer) -> dict[Loop: tuple[float | None, float | None]]:
745
+ def y_coordinate_differences(self, other: Knit_Graph_Visualizer) -> dict[Loop, tuple[float | None, float | None]]:
579
746
  """Find the differences in y-coordinates between two knitgraph visualizations. Used for testing and comparing visualization results.
580
747
 
581
748
  Args:
@@ -589,12 +756,28 @@ class Knit_Graph_Visualizer:
589
756
  ** The second value of each tuple is the y-coordinate of the loop in the other visualization or NOne if the loop is not in that visualization.
590
757
 
591
758
  """
592
- differences = {l: (self._get_y_of_loop(l), None) for l in self.data_graph.nodes if not other.data_graph.has_node(l)}
593
- differences.update({l: (None, other._get_y_of_loop(l)) for l in other.data_graph.nodes if not self.data_graph.has_node(l)})
594
- differences.update({l: (self._get_y_of_loop(l), other._get_y_of_loop(l)) for l in self.data_graph.nodes if other.data_graph.has_node(l) and self._get_y_of_loop(l) != other._get_y_of_loop(l)})
759
+ differences: dict[Loop, tuple[float | None, float | None]] = {
760
+ cast(Loop, l): (self._get_y_of_loop(l), None)
761
+ for l in self.data_graph.nodes
762
+ if not other.data_graph.has_node(l)
763
+ }
764
+ differences.update(
765
+ {
766
+ cast(Loop, l): (None, other._get_y_of_loop(l))
767
+ for l in other.data_graph.nodes
768
+ if not self.data_graph.has_node(l)
769
+ }
770
+ )
771
+ differences.update(
772
+ {
773
+ cast(Loop, l): (self._get_y_of_loop(l), other._get_y_of_loop(l))
774
+ for l in self.data_graph.nodes
775
+ if other.data_graph.has_node(l) and self._get_y_of_loop(l) != other._get_y_of_loop(l)
776
+ }
777
+ )
595
778
  return differences
596
779
 
597
- def __eq__(self, other: Knit_Graph_Visualizer) -> bool:
780
+ def __eq__(self, other: object) -> bool:
598
781
  """Two visualizations are equal if share the same x,y coordinates for all loops in the visualization and both contain the same set of loop nodes.
599
782
  Args:
600
783
  other (Knit_Graph_Visualizer): The knitgraph visualization to compare to.
@@ -602,13 +785,25 @@ class Knit_Graph_Visualizer:
602
785
  Returns:
603
786
  bool: True if the knitgraph visualizations are equal, False otherwise.
604
787
  """
605
- return len(self.data_graph.nodes) == len(other.data_graph.nodes) and len(self.x_coordinate_differences(other)) == 0 and len(self.y_coordinate_differences(other)) == 0
606
-
607
-
608
- def visualize_knit_graph(knit_graph: Knit_Graph, first_course_index: int = 0, top_course_index: int | None = None,
609
- start_on_left: bool = True, balance_by_base_width: bool = False,
610
- left_zero_align: bool = True, graph_title: str = "knit_graph",
611
- show_figure: bool = True, renderer: str | None = None) -> Figure:
788
+ return (
789
+ isinstance(other, Knit_Graph_Visualizer)
790
+ and len(self.data_graph.nodes) == len(other.data_graph.nodes)
791
+ and len(self.x_coordinate_differences(other)) == 0
792
+ and len(self.y_coordinate_differences(other)) == 0
793
+ )
794
+
795
+
796
+ def visualize_knit_graph(
797
+ knit_graph: Knit_Graph,
798
+ first_course_index: int = 0,
799
+ top_course_index: int | None = None,
800
+ start_on_left: bool = True,
801
+ balance_by_base_width: bool = False,
802
+ left_zero_align: bool = True,
803
+ graph_title: str = "knit_graph",
804
+ show_figure: bool = True,
805
+ renderer: str | None = None,
806
+ ) -> Figure:
612
807
  """Generate and optionally display a plotly visualization of the given knit graph with specified configuration.
613
808
 
614
809
  Args:
@@ -625,18 +820,25 @@ def visualize_knit_graph(knit_graph: Knit_Graph, first_course_index: int = 0, to
625
820
  Returns:
626
821
  Figure: The plotly figure object.
627
822
  """
628
- visualizer = Knit_Graph_Visualizer(knit_graph, first_course_index, top_course_index, start_on_left, balance_by_base_width, left_zero_align)
823
+ visualizer = Knit_Graph_Visualizer(
824
+ knit_graph,
825
+ first_course_index,
826
+ top_course_index,
827
+ start_on_left,
828
+ balance_by_base_width,
829
+ left_zero_align,
830
+ )
629
831
  fig = visualizer.make_figure(graph_title)
630
832
 
631
833
  if show_figure:
632
834
  try:
633
835
  # Configure display to minimize resource usage
634
836
  config = {
635
- 'displayModeBar': False, # Hide toolbar to reduce resource usage
636
- 'displaylogo': False, # Hide plotly logo
637
- 'staticPlot': False, # Keep interactive
638
- 'scrollZoom': True, # Allow zoom
639
- 'doubleClick': 'reset+autosize' # Double-click behavior
837
+ "displayModeBar": False, # Hide toolbar to reduce resource usage
838
+ "displaylogo": False, # Hide plotly logo
839
+ "staticPlot": False, # Keep interactive
840
+ "scrollZoom": True, # Allow zoom
841
+ "doubleClick": "reset+autosize", # Double-click behavior
640
842
  }
641
843
 
642
844
  if renderer:
@@ -650,9 +852,15 @@ def visualize_knit_graph(knit_graph: Knit_Graph, first_course_index: int = 0, to
650
852
  return fig
651
853
 
652
854
 
653
- def visualize_knit_graph_safe(knit_graph: Knit_Graph, first_course_index: int = 0, top_course_index: int | None = None,
654
- start_on_left: bool = True, balance_by_base_width: bool = False,
655
- left_zero_align: bool = True, graph_title: str = "knit_graph") -> Figure:
855
+ def visualize_knit_graph_safe(
856
+ knit_graph: Knit_Graph,
857
+ first_course_index: int = 0,
858
+ top_course_index: int | None = None,
859
+ start_on_left: bool = True,
860
+ balance_by_base_width: bool = False,
861
+ left_zero_align: bool = True,
862
+ graph_title: str = "knit_graph",
863
+ ) -> Figure:
656
864
  """Generate a plotly visualization of the given knit graph with specified configuration.
657
865
  This function is safe for UnitTest and other headless environments because it does not attempt to show the visualization.
658
866
 
@@ -668,8 +876,13 @@ def visualize_knit_graph_safe(knit_graph: Knit_Graph, first_course_index: int =
668
876
  Returns:
669
877
  Figure: The plotly figure object.
670
878
  """
671
- return visualize_knit_graph(knit_graph, first_course_index=first_course_index, top_course_index=top_course_index,
672
- start_on_left=start_on_left,
673
- balance_by_base_width=balance_by_base_width,
674
- left_zero_align=left_zero_align, graph_title=graph_title,
675
- show_figure=False)
879
+ return visualize_knit_graph(
880
+ knit_graph,
881
+ first_course_index=first_course_index,
882
+ top_course_index=top_course_index,
883
+ start_on_left=start_on_left,
884
+ balance_by_base_width=balance_by_base_width,
885
+ left_zero_align=left_zero_align,
886
+ graph_title=graph_title,
887
+ show_figure=False,
888
+ )