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