knit-graphs 0.0.6__py3-none-any.whl → 0.0.7__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.
Files changed (33) hide show
  1. knit_graphs-0.0.6.dist-info/licenses/LICENSE → LICENSE +21 -21
  2. README.md +75 -0
  3. docs/Makefile +20 -0
  4. docs/make.bat +35 -0
  5. docs/source/api/knit_graphs.artin_wale_braids.rst +58 -0
  6. docs/source/api/knit_graphs.rst +74 -0
  7. docs/source/conf.py +335 -0
  8. docs/source/index.rst +71 -0
  9. docs/source/installation.rst +67 -0
  10. knit_graphs/Course.py +156 -104
  11. knit_graphs/Knit_Graph.py +249 -186
  12. knit_graphs/Knit_Graph_Visualizer.py +680 -0
  13. knit_graphs/Loop.py +141 -155
  14. knit_graphs/Pull_Direction.py +68 -23
  15. knit_graphs/Yarn.py +424 -267
  16. knit_graphs/__init__.py +3 -3
  17. knit_graphs/_base_classes.py +173 -0
  18. knit_graphs/artin_wale_braids/Crossing_Direction.py +74 -15
  19. knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +95 -62
  20. knit_graphs/artin_wale_braids/Wale.py +169 -93
  21. knit_graphs/artin_wale_braids/Wale_Braid.py +50 -30
  22. knit_graphs/artin_wale_braids/Wale_Braid_Word.py +99 -54
  23. knit_graphs/artin_wale_braids/Wale_Group.py +136 -88
  24. knit_graphs/{knit_graph_generators/basic_knit_graph_generators.py → basic_knit_graph_generators.py} +302 -248
  25. knit_graphs-0.0.7.dist-info/LICENSE +21 -0
  26. {knit_graphs-0.0.6.dist-info → knit_graphs-0.0.7.dist-info}/METADATA +33 -24
  27. knit_graphs-0.0.7.dist-info/RECORD +29 -0
  28. {knit_graphs-0.0.6.dist-info → knit_graphs-0.0.7.dist-info}/WHEEL +1 -1
  29. knit_graphs/__about__.py +0 -4
  30. knit_graphs/knit_graph_generators/__init__.py +0 -0
  31. knit_graphs/knit_graph_visualizer/Stitch_Visualizer.py +0 -427
  32. knit_graphs/knit_graph_visualizer/__init__.py +0 -0
  33. knit_graphs-0.0.6.dist-info/RECORD +0 -22
@@ -0,0 +1,680 @@
1
+ """Module used to visualize a Knit graph with the plotly graph object library.
2
+
3
+ This module provides comprehensive visualization capabilities for knit graphs using Plotly.
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
+ """
6
+ from typing import Iterable, cast
7
+
8
+ from networkx import DiGraph
9
+ from plotly.graph_objs import Figure, Layout, Scatter
10
+
11
+ from knit_graphs.artin_wale_braids.Crossing_Direction import Crossing_Direction
12
+ from knit_graphs.Course import Course
13
+ from knit_graphs.Knit_Graph import Knit_Graph
14
+ from knit_graphs.Loop import Loop
15
+ from knit_graphs.Pull_Direction import Pull_Direction
16
+
17
+
18
+ class Knit_Graph_Visualizer:
19
+ """A class used to visualize a knit graph using the plotly graph objects library.
20
+
21
+ This class converts knit graph data structures into interactive 2D visualizations by calculating loop positions,
22
+ rendering yarn paths, and displaying stitch relationships with appropriate styling for different stitch types and cable crossings.
23
+
24
+ Attributes:
25
+ knit_graph (Knit_Graph): The knit graph to visualize.
26
+ courses (list[Course]): List of courses (horizontal rows) in the knit graph.
27
+ base_width (float): The width of the base course used for scaling.
28
+ base_left (float): The leftmost position of the base course.
29
+ loops_to_course (dict[Loop, Course]): Mapping from loops to their containing courses.
30
+ data_graph (DiGraph): Internal graph for storing loop positions and visualization data.
31
+ left_zero_align (bool): Whether to align the left edge of courses to zero.
32
+ balance_by_base_width (bool): Whether to scale course widths to match the base course.
33
+ start_on_left (bool): Whether to start knitting visualization from the left side.
34
+ top_course_index (int): The index of the topmost course to visualize.
35
+ first_course_index (int): The index of the first (bottom) course to visualize.
36
+ """
37
+
38
+ def __init__(self, knit_graph: Knit_Graph, first_course_index: int = 0, top_course_index: int | None = None,
39
+ start_on_left: bool = True,
40
+ balance_by_base_width: bool = False,
41
+ left_zero_align: bool = True):
42
+ """Initialize the knit graph visualizer with specified configuration options.
43
+
44
+ Args:
45
+ knit_graph (Knit_Graph): The knit graph to be visualized.
46
+ first_course_index (int, optional): The index of the first course to include in the visualization. Defaults to 0.
47
+ top_course_index (int | None, optional): The index of the last course to include in the visualization. If None, includes all courses up to the top.
48
+ start_on_left (bool, optional): Whether to position the first loop on the left side of the visualization. Defaults to True.
49
+ balance_by_base_width (bool, optional): Whether to scale all course widths to match the base course width. Defaults to False.
50
+ left_zero_align (bool, optional): Whether to align the leftmost loop of each course to x=0. Defaults to True.
51
+ """
52
+ self.left_zero_align: bool = left_zero_align
53
+ self.balance_by_base_width: bool = balance_by_base_width
54
+ self.start_on_left: bool = start_on_left
55
+ self.knit_graph: Knit_Graph = knit_graph
56
+ self.courses: list[Course] = knit_graph.get_courses()
57
+ if top_course_index is None:
58
+ top_course_index = len(self.courses)
59
+ self.top_course_index: int = top_course_index
60
+ self.first_course_index: int = first_course_index
61
+ self.base_width: float = float(len(self.courses[first_course_index])) # Updates when creating base course.
62
+ self.base_left: float = 0.0 # Updates when creating the base course.
63
+ self.loops_to_course: dict[Loop, Course] = {}
64
+ for course in self.courses:
65
+ self.loops_to_course.update({loop: course for loop in course})
66
+ self.data_graph: DiGraph = DiGraph()
67
+ self._loops_need_placement: set[Loop] = set()
68
+ self._loop_markers: list[Scatter] = []
69
+ self._yarn_traces: list[Scatter] = []
70
+ self._top_knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
71
+ self._bot_knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
72
+ self._top_purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
73
+ self._bot_purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
74
+ self._knit_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
75
+ self._purl_trace_data: dict[str: list[float] | list[tuple[Loop, Loop] | bool]] = {'x': [], 'y': [], 'edge': [], 'is_start': []}
76
+ # Form the visualization.
77
+ self._position_loops()
78
+ self._set_loop_markers()
79
+ self._set_yarn_traces()
80
+ self._add_stitch_edges()
81
+
82
+ def make_figure(self, graph_title: str = "Knit Graph") -> None:
83
+ """Generate and display the interactive figure to visualize this knit graph.
84
+
85
+ Args:
86
+ graph_title (str, optional): The title to display on the figure. Defaults to "Knit Graph".
87
+ """
88
+ go_layout = Layout(title=graph_title,
89
+ showlegend=True,
90
+ hovermode='closest',
91
+ margin=dict(b=20, l=5, r=5, t=40)
92
+ )
93
+ figure_data = [self._top_knit_trace(), self._top_purl_trace(),
94
+ self._no_cross_knit_trace(), self._no_cross_purl_trace(),
95
+ self._bot_knit_trace(), self._bot_purl_trace()]
96
+ figure_data.extend(self._yarn_traces)
97
+ figure_data.extend(self._loop_markers)
98
+ fig = Figure(data=figure_data,
99
+ layout=go_layout)
100
+ fig.show()
101
+
102
+ def _no_cross_knit_trace(self, line_width: float = 4.0, knit_color: str = 'blue') -> Scatter:
103
+ """Create a scatter trace for knit stitches not involved in cable crossings.
104
+
105
+ Args:
106
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 4.0.
107
+ knit_color (str, optional): The color of knit stitches in the visualization. Defaults to 'blue'.
108
+
109
+ Returns:
110
+ Scatter: The plotly scatter object used to visualize knit stitches not involved in cables.
111
+ """
112
+ return self._stitch_trace(self._knit_trace_data, "Knit Stitches", knit_color, line_width, opacity=0.8)
113
+
114
+ def _top_knit_trace(self, line_width: float = 5.0, knit_color: str = 'blue') -> Scatter:
115
+ """Create a scatter trace for knit stitches that cross over other stitches in cables.
116
+
117
+ Args:
118
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 5.0.
119
+ knit_color (str, optional): The color of knit stitches in the visualization. Defaults to 'blue'.
120
+
121
+ Returns:
122
+ Scatter: The plotly scatter object used to visualize knit stitches on top of cable crossings.
123
+ """
124
+ return self._stitch_trace(self._top_knit_trace_data, "Knit Stitches on Top of Cable", knit_color, line_width, opacity=1.0)
125
+
126
+ def _bot_knit_trace(self, line_width: float = 3.0, knit_color: str = 'blue') -> Scatter:
127
+ """Create a scatter trace for knit stitches that cross under other stitches in cables.
128
+
129
+ Args:
130
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 3.0.
131
+ knit_color (str, optional): The color of knit stitches in the visualization. Defaults to 'blue'.
132
+
133
+ Returns:
134
+ Scatter: The plotly scatter object used to visualize knit stitches below cable crossings.
135
+ """
136
+ return self._stitch_trace(self._bot_knit_trace_data, "Knit Stitches Below Cable", knit_color, line_width, opacity=.5)
137
+
138
+ def _no_cross_purl_trace(self, line_width: float = 4.0, purl_color: str = 'red') -> Scatter:
139
+ """Create a scatter trace for purl stitches not involved in cable crossings.
140
+
141
+ Args:
142
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 4.0.
143
+ purl_color (str, optional): The color of purl stitches in the visualization. Defaults to 'red'.
144
+
145
+ Returns:
146
+ Scatter: The plotly scatter object used to visualize purl stitches not involved in cables.
147
+ """
148
+ return self._stitch_trace(self._purl_trace_data, "Purl Stitches", purl_color, line_width, opacity=0.8)
149
+
150
+ def _top_purl_trace(self, line_width: float = 5.0, purl_color: str = 'red') -> Scatter:
151
+ """Create a scatter trace for purl stitches that cross over other stitches in cables.
152
+
153
+ Args:
154
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 5.0.
155
+ purl_color (str, optional): The color of purl stitches in the visualization. Defaults to 'red'.
156
+
157
+ Returns:
158
+ Scatter: The plotly scatter object used to visualize purl stitches on top of cable crossings.
159
+ """
160
+ return self._stitch_trace(self._top_purl_trace_data, "Purl Stitches on Top of Cable", purl_color, line_width, opacity=1.0)
161
+
162
+ def _bot_purl_trace(self, line_width: float = 3.0, purl_color: str = 'blue') -> Scatter:
163
+ """Create a scatter trace for purl stitches that cross under other stitches in cables.
164
+
165
+ Args:
166
+ line_width (float, optional): The width of the lines representing the stitch edges. Defaults to 3.0.
167
+ purl_color (str, optional): The color of purl stitches in the visualization. Defaults to 'blue'.
168
+
169
+ Returns:
170
+ Scatter: The plotly scatter object used to visualize purl stitches below cable crossings.
171
+ """
172
+ return self._stitch_trace(self._bot_purl_trace_data, "Purl Stitches Below Cable", purl_color, line_width, opacity=.5)
173
+
174
+ @staticmethod
175
+ 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:
176
+ """Create a generic scatter trace for stitch visualization with specified styling.
177
+
178
+ Args:
179
+ trace_data (dict): The trace data containing x, y coordinates and edge information to be plotted.
180
+ trace_name (str): The name of the trace to show in the figure legend.
181
+ trace_color (str): The color of the trace lines.
182
+ line_width (float): The width of lines representing the stitch edges.
183
+ opacity (float): The opacity of the trace lines (0.0 to 1.0).
184
+
185
+ Returns:
186
+ Scatter: The plotly scatter object configured to visualize the given stitch traces.
187
+ """
188
+ return Scatter(name=trace_name,
189
+ x=trace_data['x'], y=trace_data['y'],
190
+ line=dict(width=line_width, color=trace_color, dash='solid'),
191
+ opacity=opacity,
192
+ mode='lines')
193
+
194
+ def _add_cable_edges(self) -> None:
195
+ """Add all stitch edges that are involved in cable crossings to the appropriate trace data.
196
+
197
+ This method processes all cable crossings in the knit graph and adds the associated stitch edges to the correct trace data based on their crossing direction (over/under).
198
+ """
199
+ for left_loop, right_loop in self.knit_graph.braid_graph.loop_crossing_graph.edges:
200
+ crossing_direction = self.knit_graph.braid_graph.get_crossing(left_loop, right_loop)
201
+ for left_parent in left_loop.parent_loops:
202
+ self._add_stitch_edge(left_parent, left_loop, crossing_direction)
203
+ for right_parent in right_loop.parent_loops:
204
+ self._add_stitch_edge(right_parent, right_loop, ~crossing_direction)
205
+
206
+ def _add_stitch_edges(self) -> None:
207
+ """Add all stitch edges to the visualization trace data based on their type and cable position.
208
+
209
+ This method first adds cable-involved edges, then adds all remaining stitch edges as non-cable stitches. It ensures all visible stitch connections are properly categorized for rendering.
210
+ """
211
+ self._add_cable_edges()
212
+ # Add remaining stitches as though they have no cable crossing.
213
+ for u, v in self.knit_graph.stitch_graph.edges:
214
+ if (not self._stitch_has_position(u, v) # This edge has not been placed
215
+ and self._loop_has_position(u) and self._loop_has_position(v)): # Both loops do have positions.
216
+ self._add_stitch_edge(u, v, Crossing_Direction.No_Cross)
217
+
218
+ def _add_stitch_edge(self, u: Loop, v: Loop, crossing_direction: Crossing_Direction) -> None:
219
+ """Add a single stitch edge to the appropriate trace data based on stitch type and cable crossing.
220
+
221
+ Args:
222
+ u (Loop): The parent loop in the stitch connection.
223
+ v (Loop): The child loop in the stitch connection.
224
+ crossing_direction (Crossing_Direction): The cable crossing direction of this stitch edge.
225
+ """
226
+ pull_direction = self.knit_graph.get_pull_direction(u, v)
227
+ if pull_direction is None:
228
+ return # No edge between these loops
229
+ elif pull_direction is Pull_Direction.BtF: # Knit Stitch:
230
+ if crossing_direction is Crossing_Direction.Over_Right:
231
+ trace_data = self._top_knit_trace_data
232
+ elif crossing_direction is Crossing_Direction.Under_Right:
233
+ trace_data = self._bot_knit_trace_data
234
+ else:
235
+ trace_data = self._knit_trace_data
236
+ else:
237
+ if crossing_direction is Crossing_Direction.Over_Right:
238
+ trace_data = self._top_purl_trace_data
239
+ elif crossing_direction is Crossing_Direction.Under_Right:
240
+ trace_data = self._bot_purl_trace_data
241
+ else:
242
+ trace_data = self._purl_trace_data
243
+ self.data_graph.add_edge(u, v, pull_direction=pull_direction)
244
+ trace_data['x'].append(self._get_x_of_loop(u))
245
+ trace_data['y'].append(self._get_y_of_loop(u))
246
+ trace_data['edge'].append((u, v))
247
+ trace_data['is_start'].append(True)
248
+ trace_data['x'].append(self._get_x_of_loop(v))
249
+ trace_data['y'].append(self._get_y_of_loop(v))
250
+ trace_data['edge'].append((u, v))
251
+ trace_data['is_start'].append(False)
252
+ trace_data['x'].append(None)
253
+ trace_data['y'].append(None)
254
+
255
+ def _set_loop_markers(self, loop_size: float = 30.0, loop_border_width: float = 2.0) -> None:
256
+ """Create plotly scatter objects to mark the position of each loop in the visualization.
257
+
258
+ Args:
259
+ loop_size (float, optional): The diameter of the circle marking each loop. Defaults to 30.0.
260
+ loop_border_width (float, optional): The width of the border around each loop marker. Defaults to 2.0.
261
+ """
262
+ yarns_to_loop_data = {yarn: {'x': [self._get_x_of_loop(loop) for loop in yarn],
263
+ 'y': [self._get_y_of_loop(loop) for loop in yarn],
264
+ 'loop_id': [loop.loop_id for loop in yarn]
265
+ }
266
+ for yarn in self.knit_graph.yarns
267
+ }
268
+ self._loop_markers = [Scatter(name=f"Loops on {yarn.yarn_id}", x=yarn_data['x'], y=yarn_data['y'], text=yarn_data['loop_id'],
269
+ textposition='middle center',
270
+ mode='markers+text',
271
+ marker=dict(
272
+ reversescale=True,
273
+ color=yarn.properties.color,
274
+ size=loop_size,
275
+ line_width=loop_border_width))
276
+ for yarn, yarn_data in yarns_to_loop_data.items()]
277
+
278
+ def _set_yarn_traces(self, line_width: float = 1.0, smoothing: float = 1.3) -> None:
279
+ """Create plotly traces representing the path of each yarn through the knitted structure.
280
+
281
+ Args:
282
+ line_width (float, optional): The width of the lines representing yarn paths. Defaults to 1.0.
283
+ smoothing (float, optional): The smoothing factor for spline interpolation of yarn paths. Defaults to 1.3.
284
+ """
285
+ yarns_to_float_data = {}
286
+ for yarn in self.knit_graph.yarns:
287
+ float_data: dict[str, list[float]] = {'x': [], 'y': []}
288
+ for u in yarn:
289
+ if self._loop_has_position(u):
290
+ float_data['x'].append(self._get_x_of_loop(u))
291
+ float_data['y'].append(self._get_y_of_loop(u))
292
+ yarns_to_float_data[yarn] = float_data
293
+ self._yarn_traces = [Scatter(name=yarn.yarn_id,
294
+ x=float_data['x'], y=float_data['y'],
295
+ line=dict(width=line_width,
296
+ color=yarn.properties.color,
297
+ shape='spline',
298
+ smoothing=smoothing),
299
+ mode='lines')
300
+ for yarn, float_data in yarns_to_float_data.items()]
301
+
302
+ def _position_loops(self) -> None:
303
+ """Calculate and set the x,y coordinate positions of all loops to be visualized.
304
+
305
+ This method orchestrates the complete positioning process including base course positioning, subsequent course positioning, knit/purl shifting, and float alignment adjustments.
306
+ """
307
+ self._position_base_course()
308
+ self._place_loops_in_courses()
309
+ self._shift_knit_purl()
310
+ self._shift_loops_by_float_alignment()
311
+
312
+ def _shift_knit_purl(self, shift: float = 0.1) -> None:
313
+ """Adjust the horizontal position of loops to visually distinguish knit from purl stitches.
314
+
315
+ This method shifts knit stitches slightly left and purl stitches slightly right to create visual separation that makes knit-purl patterns more distinct in the visualization.
316
+
317
+ Args:
318
+ shift (float, optional): The amount to shift stitches horizontally. Knit stitches are shifted left and purl stitches are shifted right. Defaults to 0.1.
319
+ """
320
+ 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)
321
+ 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)
322
+ if not (has_knits and has_purls):
323
+ return # Don't make any changes, because all stitches are of the same type.
324
+ yarn_over_align = set()
325
+ for loop in self.data_graph.nodes:
326
+ if not loop.has_parent_loops(): # Yarn-over
327
+ if self.knit_graph.has_child_loop(loop): # Align yarn-overs with one child to its child
328
+ yarn_over_align.add(loop)
329
+ continue # Don't shift yarn-overs
330
+ knit_parents = len([u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.BtF])
331
+ purl_parents = len([u for u in loop.parent_loops if self.knit_graph.get_pull_direction(u, loop) is Pull_Direction.FtB])
332
+ if knit_parents > purl_parents: # Shift the loop as though it is being knit.
333
+ self._set_x_of_loop(loop, self._get_x_of_loop(loop) - shift)
334
+ elif purl_parents > knit_parents: # Shift the loop as though it is being purled.
335
+ self._set_x_of_loop(loop, self._get_x_of_loop(loop) + shift)
336
+
337
+ for loop in yarn_over_align:
338
+ child_loop = self.knit_graph.get_child_loop(loop)
339
+ assert isinstance(child_loop, Loop)
340
+ self._set_x_of_loop(loop, self._get_x_of_loop(child_loop))
341
+
342
+ def _shift_loops_by_float_alignment(self, float_increment: float = 0.25) -> None:
343
+ """Adjust the vertical position of loops based on their float relationships.
344
+
345
+ Loops that pass in front of floats are shifted down while loops that pass behind floats are shifted up.
346
+ This creates a visual layering effect that represents the three-dimensional structure of the knitted fabric.
347
+
348
+ Args:
349
+ float_increment (float, optional): The vertical spacing adjustment for loops relative to floats they cross. Defaults to 0.25.
350
+ """
351
+ for yarn in self.knit_graph.yarns:
352
+ for u, v, front_loops in yarn.loops_in_front_of_floats():
353
+ for front_loop in front_loops:
354
+ 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
355
+ 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.
356
+ for u, v, back_loops in yarn.loops_behind_floats():
357
+ for back_loop in back_loops:
358
+ 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
359
+ 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.
360
+
361
+ def _get_course_of_loop(self, loop: Loop) -> Course:
362
+ """Get the course (horizontal row) that contains the specified loop.
363
+
364
+ Args:
365
+ loop (Loop): The loop to find the course for.
366
+
367
+ Returns:
368
+ Course: The course that contains this loop.
369
+ """
370
+ return self.loops_to_course[loop]
371
+
372
+ def _place_loop(self, loop: Loop, x: float, y: float) -> None:
373
+ """Add a loop to the visualization data graph at the specified coordinates.
374
+
375
+ If the loop already exists in the data graph, its coordinates will be updated to the new position.
376
+
377
+ Args:
378
+ loop (Loop): The loop to position in the visualization.
379
+ x (float): The x coordinate for the loop.
380
+ y (float): The y coordinate for the loop.
381
+ """
382
+ if self._loop_has_position(loop):
383
+ self._set_x_of_loop(loop, x)
384
+ self._set_y_of_loop(loop, y)
385
+ else:
386
+ self.data_graph.add_node(loop, x=x, y=y)
387
+
388
+ def _set_x_of_loop(self, loop: Loop, x: float) -> None:
389
+ """Update the x coordinate of a loop that already exists in the visualization data graph.
390
+
391
+ Args:
392
+ loop (Loop): The loop to update the x coordinate for.
393
+ x (float): The new x coordinate for the loop.
394
+
395
+ Raises:
396
+ KeyError: If the loop does not exist in the visualization data graph.
397
+ """
398
+ if self._loop_has_position(loop):
399
+ self.data_graph.nodes[loop]['x'] = x
400
+ else:
401
+ raise KeyError(f"Loop {loop} is not in the data graph")
402
+
403
+ def _set_y_of_loop(self, loop: Loop, y: float) -> None:
404
+ """Update the y coordinate of a loop that already exists in the visualization data graph.
405
+
406
+ Args:
407
+ loop (Loop): The loop to update the y coordinate for.
408
+ y (float): The new y coordinate for the loop.
409
+
410
+ Raises:
411
+ KeyError: If the loop does not exist in the visualization data graph.
412
+ """
413
+ if self._loop_has_position(loop):
414
+ self.data_graph.nodes[loop]['y'] = y
415
+ else:
416
+ raise KeyError(f"Loop {loop} is not in the data graph")
417
+
418
+ def _get_x_of_loop(self, loop: Loop) -> float:
419
+ """Get the x coordinate of a loop from the visualization data graph.
420
+
421
+ Args:
422
+ loop (Loop): The loop to get the x coordinate for.
423
+
424
+ Returns:
425
+ float: The x coordinate of the loop.
426
+
427
+ Raises:
428
+ KeyError: If the loop does not exist in the visualization data graph.
429
+ """
430
+ if self._loop_has_position(loop):
431
+ return float(self.data_graph.nodes[loop]['x'])
432
+ else:
433
+ raise KeyError(f"Loop {loop} is not in the data graph")
434
+
435
+ def _get_y_of_loop(self, loop: Loop) -> float:
436
+ """Get the y coordinate of a loop from the visualization data graph.
437
+
438
+ Args:
439
+ loop (Loop): The loop to get the y coordinate for.
440
+
441
+ Returns:
442
+ float: The y coordinate of the loop.
443
+
444
+ Raises:
445
+ KeyError: If the loop does not exist in the visualization data graph.
446
+ """
447
+ if self._loop_has_position(loop):
448
+ return float(self.data_graph.nodes[loop]['y'])
449
+ else:
450
+ raise KeyError(f"Loop {loop} is not in the data graph")
451
+
452
+ def _loop_has_position(self, loop: Loop) -> bool:
453
+ """Check if a loop has been positioned in the visualization data graph.
454
+
455
+ Args:
456
+ loop (Loop): The loop to check for positioning.
457
+
458
+ Returns:
459
+ bool: True if the loop has been positioned in the visualization, False otherwise.
460
+ """
461
+ return bool(self.data_graph.has_node(loop))
462
+
463
+ def _stitch_has_position(self, u: Loop, v: Loop) -> bool:
464
+ """Check if a stitch edge between two loops has been added to the visualization data graph.
465
+
466
+ Args:
467
+ u (Loop): The parent loop in the stitch edge.
468
+ v (Loop): The child loop in the stitch edge.
469
+
470
+ Returns:
471
+ bool: True if a stitch edge from u to v exists in the visualization data graph, False otherwise.
472
+ """
473
+ return bool(self.data_graph.has_edge(u, v))
474
+
475
+ def _place_loops_in_courses(self, course_spacing: float = 1.0) -> None:
476
+ """Position loops in all courses above the base course using parent relationships and yarn connections.
477
+
478
+ Args:
479
+ course_spacing (float, optional): The vertical distance between consecutive courses. Defaults to 1.0.
480
+ """
481
+ y = course_spacing
482
+ for course in self.courses[self.first_course_index + 1:self.top_course_index]:
483
+ self._place_loops_by_parents(course, y)
484
+ self._swap_loops_in_cables(course)
485
+ self._left_align_course(course)
486
+ self._balance_course(course)
487
+ y += course_spacing # Shift y coordinate up with each course
488
+
489
+ def _swap_loops_in_cables(self, course: Course) -> None:
490
+ """Swap the horizontal positions of loops involved in cable crossings within a course.
491
+
492
+ Args:
493
+ course (Course): The course containing loops that may need position swapping due to cable crossings.
494
+ """
495
+ for left_loop in course:
496
+ for right_loop in self.knit_graph.braid_graph.left_crossing_loops(left_loop):
497
+ crossing_direction = self.knit_graph.braid_graph.get_crossing(left_loop, right_loop)
498
+ if crossing_direction is not Crossing_Direction.No_Cross: # Swap the position of loops that cross each other.
499
+ left_x = self._get_x_of_loop(left_loop)
500
+ self._set_x_of_loop(left_loop, self._get_x_of_loop(right_loop))
501
+ self._set_x_of_loop(right_loop, left_x)
502
+
503
+ def _place_loops_by_parents(self, course: Course, y: float) -> None:
504
+ """Position loops in a course based on the average position of their parent loops.
505
+
506
+ Args:
507
+ course (Course): The course containing loops to be positioned.
508
+ y (float): The y coordinate for all loops in this course.
509
+ """
510
+ for x, loop in enumerate(course):
511
+ self._set_loop_x_by_parent_average(loop, y)
512
+ placed_loops = set()
513
+ for loop in self._loops_need_placement:
514
+ placed = self._set_loop_between_yarn_neighbors(loop, y)
515
+ if placed:
516
+ placed_loops.add(loop)
517
+ self._loops_need_placement.difference_update(placed_loops)
518
+ assert len(self._loops_need_placement) == 0, f"Loops {self._loops_need_placement} remain unplaced."
519
+ # A loops past the first course should have at least one yarn neighbor to place them.
520
+
521
+ def _set_loop_x_by_parent_average(self, loop: Loop, y: float) -> None:
522
+ """Set the x coordinate of a loop based on the weighted average position of its parent loops.
523
+
524
+ If the loop has no parent loops, it is added to the set of loops that need positioning through other methods.
525
+
526
+ Args:
527
+ loop (Loop): The loop to position based on its parents.
528
+ y (float): The y coordinate to assign to the loop.
529
+ """
530
+ if len(loop.parent_loops) == 0:
531
+ self._loops_need_placement.add(loop)
532
+ return
533
+
534
+ def _parent_weight(stack_position: int) -> float:
535
+ return float(len(loop.parent_loops) - stack_position)
536
+
537
+ parent_positions = {self._get_x_of_loop(parent_loop) * _parent_weight(stack_pos): # position of parents weighted by their stack position.
538
+ _parent_weight(stack_pos) # weight of the stack position.
539
+ for stack_pos, parent_loop in enumerate(loop.parent_loops)
540
+ if self.data_graph.has_node(parent_loop)} # Only include parent loops that are positioned.
541
+ x = sum(parent_positions.keys()) / sum(parent_positions.values())
542
+ self._place_loop(loop, x=x, y=y)
543
+
544
+ def _set_loop_between_yarn_neighbors(self, loop: Loop, y: float, spacing: float = 1.0) -> bool:
545
+ """Position a loop based on the average position of its neighboring loops along the yarn.
546
+
547
+ Args:
548
+ loop (Loop): The loop to position based on its yarn neighbors.
549
+ y (float): The y coordinate to assign to the loop.
550
+ spacing (float, optional): The minimum spacing to maintain between adjacent loops on the same course. Defaults to 1.0.
551
+
552
+ Returns:
553
+ bool: True if the loop was successfully positioned, False if no yarn neighbors were available for positioning.
554
+ """
555
+ spacing = abs(spacing) # Ensure spacing is positive.
556
+ x_neighbors = []
557
+ prior_loop = loop.prior_loop_on_yarn()
558
+ next_loop = loop.next_loop_on_yarn()
559
+ if prior_loop is not None and self._loop_has_position(prior_loop):
560
+ if self._get_y_of_loop(prior_loop) == y: # Include the spacing to ensure these are not at overlapping positions.
561
+ x_neighbors.append(self._get_x_of_loop(prior_loop) + spacing)
562
+ else: # Don't include spacing because the prior loop is on the prior course.
563
+ x_neighbors.append(self._get_x_of_loop(prior_loop))
564
+ if next_loop is not None and self._loop_has_position(next_loop):
565
+ if self._get_y_of_loop(next_loop) == y: # Include the spacing to ensure these are not at overlapping positions.
566
+ x_neighbors.append(self._get_x_of_loop(next_loop) - spacing)
567
+ else: # Don't include spacing because the prior loop is on the prior course.
568
+ x_neighbors.append(self._get_x_of_loop(next_loop))
569
+ if len(x_neighbors) == 0:
570
+ return False
571
+ x = (sum(x_neighbors) / float(len(x_neighbors))) # the average of the two neighbors
572
+ self._place_loop(loop, x=x, y=y)
573
+ return True
574
+
575
+ def _position_base_course(self) -> None:
576
+ """Position the loops in the bottom course of the visualization and establish base metrics.
577
+
578
+ This method determines whether the base course is knit in the round (tube) or in rows (flat) and positions loops accordingly.
579
+ It also establishes the base width and left position for scaling other courses.
580
+ """
581
+ base_course = self.courses[self.first_course_index]
582
+ if (len(self.courses) > self.first_course_index + 1 # There are more courses to show after the base course
583
+ 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.
584
+ self._get_base_round_course_positions(base_course)
585
+ else:
586
+ self._get_base_row_course_positions(base_course)
587
+ self._left_align_course(base_course)
588
+ self.base_left = min(self._get_x_of_loop(loop) for loop in base_course)
589
+ max_x = max(self._get_x_of_loop(loop) for loop in base_course)
590
+ self.base_width = max_x - self.base_left
591
+
592
+ def _get_base_round_course_positions(self, base_course: Course, loop_space: float = 1.0, back_shift: float = 0.5) -> None:
593
+ """Position loops in the base course for circular/tube knitting structure.
594
+
595
+ Args:
596
+ base_course (Course): The base course to position for circular knitting.
597
+ loop_space (float, optional): The horizontal spacing between loops in the course. Defaults to 1.0.
598
+ back_shift (float, optional): The vertical offset for loops on the back of the tube. Defaults to 0.5.
599
+ """
600
+ split_index = len(base_course) // 2 # Split the course in half to form a tube.
601
+ front_loops: list[Loop] = cast(list[Loop], base_course[:split_index])
602
+ front_set: set[Loop] = set(front_loops)
603
+ back_loops: list[Loop] = cast(list[Loop], base_course[split_index:])
604
+ if self.start_on_left:
605
+ back_loops = [*reversed(back_loops)]
606
+ else:
607
+ front_loops = [*reversed(front_loops)]
608
+ for x, l in enumerate(front_loops):
609
+ self._place_loop(l, x=x, y=0)
610
+ for x, back_loop in enumerate(back_loops):
611
+ float_positions = [self._get_x_of_loop(front_loop) for front_loop in back_loop.front_floats if front_loop in front_set]
612
+ 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.
613
+ self._place_loop(back_loop, x=sum(float_positions) / float(len(float_positions)), y=0.0)
614
+ elif self.start_on_left:
615
+ self._place_loop(back_loop, x=(x * loop_space) + back_shift, y=0)
616
+ else:
617
+ self._place_loop(back_loop, x=(x * loop_space) - back_shift, y=0)
618
+
619
+ def _get_base_row_course_positions(self, base_course: Course, loop_space: float = 1.0) -> None:
620
+ """Position loops in the base course for flat/row knitting structure.
621
+
622
+ Args:
623
+ base_course (Course): The base course to position for flat knitting.
624
+ loop_space (float, optional): The horizontal spacing between loops. Defaults to 1.0.
625
+ """
626
+ loops: Iterable[Loop] = list(base_course)
627
+ if not self.start_on_left:
628
+ loops = reversed(base_course)
629
+ for x, loop in enumerate(loops):
630
+ self._place_loop(loop, x=x * loop_space, y=0)
631
+
632
+ def _left_align_course(self, course: Course) -> None:
633
+ """Align the leftmost loop of a course to x=0 if left alignment is enabled.
634
+
635
+ The relative positions of all loops in the course are preserved while shifting the entire course horizontally.
636
+
637
+ Args:
638
+ course (Course): The course to potentially left-align.
639
+ """
640
+ if self.left_zero_align:
641
+ current_left = min(self._get_x_of_loop(loop) for loop in course)
642
+ if current_left != 0.0:
643
+ for loop in course:
644
+ self._set_x_of_loop(loop, self._get_x_of_loop(loop) - current_left)
645
+
646
+ def _balance_course(self, course: Course) -> None:
647
+ """Scale the width of a course to match the base course width if balancing is enabled.
648
+
649
+ Args:
650
+ course (Course): The course to potentially balance to match the base width.
651
+ """
652
+ current_left = min(self._get_x_of_loop(loop) for loop in course)
653
+ max_x = max(self._get_x_of_loop(loop) for loop in course)
654
+ course_width = max_x - current_left
655
+ if self.balance_by_base_width and course_width != self.base_width:
656
+ def _target_distance_from_left(l: Loop) -> float:
657
+ current_distance_from_left = self._get_x_of_loop(l) - current_left
658
+ return (current_distance_from_left * self.base_width) / course_width
659
+
660
+ for loop in course:
661
+ self._set_x_of_loop(loop, _target_distance_from_left(loop) + current_left)
662
+
663
+
664
+ def visualize_knit_graph(knit_graph: Knit_Graph, first_course_index: int = 0, top_course_index: int | None = None, start_on_left: bool = True, balance_by_base_width: bool = False,
665
+ left_zero_align: bool = True, graph_title: str = "knit_graph", show_figure: bool = True) -> None:
666
+ """Generate and display a plotly visualization of the given knit graph with specified configuration.
667
+
668
+ Args:
669
+ knit_graph (Knit_Graph): The knit graph to visualize.
670
+ first_course_index (int): Index of the first (bottom) course to include in the visualization. Defaults to 0.
671
+ top_course_index (int | None): Index of the last (top) course to include in the visualization. If None, visualizes up to the top of the knit graph.
672
+ start_on_left (bool): Whether the first loop knit is presumed to be positioned on the left of the pattern. Defaults to True.
673
+ balance_by_base_width (bool): Whether to scale all course widths to match the first course width. Defaults to False.
674
+ left_zero_align (bool): Whether to align the leftmost position of each course with x=0 in the figure. Defaults to True.
675
+ graph_title (str): The title to display on the generated figure. Defaults to "knit_graph".
676
+ show_figure (bool, optional): If True, the visualization will be shown. Defaults to True.
677
+ """
678
+ visualizer = Knit_Graph_Visualizer(knit_graph, first_course_index, top_course_index, start_on_left, balance_by_base_width, left_zero_align)
679
+ if show_figure:
680
+ visualizer.make_figure(graph_title)