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.
- docs/source/conf.py +71 -71
- docs/source/index.rst +0 -4
- knit_graphs/Course.py +3 -1
- knit_graphs/Knit_Graph.py +17 -11
- knit_graphs/Knit_Graph_Visualizer.py +362 -149
- knit_graphs/Loop.py +3 -2
- knit_graphs/Pull_Direction.py +2 -0
- knit_graphs/Yarn.py +35 -10
- knit_graphs/artin_wale_braids/Crossing_Direction.py +2 -0
- knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +16 -5
- knit_graphs/artin_wale_braids/Wale.py +16 -7
- knit_graphs/artin_wale_braids/Wale_Braid.py +1 -0
- knit_graphs/artin_wale_braids/Wale_Braid_Word.py +9 -8
- knit_graphs/artin_wale_braids/Wale_Group.py +8 -5
- knit_graphs/basic_knit_graph_generators.py +45 -10
- knit_graphs/py.typed +0 -0
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.11.dist-info}/METADATA +3 -2
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.11.dist-info}/RECORD +20 -19
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.11.dist-info}/LICENSE +0 -0
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.11.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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__(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
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 =
|
|
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(
|
|
215
|
-
|
|
216
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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 (
|
|
263
|
-
|
|
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[
|
|
287
|
-
trace_data[
|
|
288
|
-
trace_data[
|
|
289
|
-
trace_data[
|
|
290
|
-
trace_data[
|
|
291
|
-
trace_data[
|
|
292
|
-
trace_data[
|
|
293
|
-
trace_data[
|
|
294
|
-
trace_data[
|
|
295
|
-
trace_data[
|
|
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 = {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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]] = {
|
|
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[
|
|
323
|
-
float_data[
|
|
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 = [
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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(
|
|
344
|
-
|
|
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(
|
|
354
|
-
|
|
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(
|
|
371
|
-
|
|
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(
|
|
375
|
-
|
|
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][
|
|
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][
|
|
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][
|
|
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][
|
|
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
|
|
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
|
|
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 = {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
500
|
-
|
|
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(
|
|
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,
|
|
520
|
-
self._place_loop(
|
|
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 = [
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
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:
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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(
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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(
|
|
654
|
-
|
|
655
|
-
|
|
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(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
)
|