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.
- docs/source/conf.py +71 -71
- docs/source/index.rst +0 -4
- knit_graphs/Course.py +64 -41
- knit_graphs/Knit_Graph.py +70 -162
- knit_graphs/Knit_Graph_Visualizer.py +386 -185
- knit_graphs/Loop.py +124 -117
- knit_graphs/Pull_Direction.py +5 -2
- knit_graphs/Yarn.py +257 -219
- knit_graphs/artin_wale_braids/Crossing_Direction.py +2 -0
- knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +47 -56
- knit_graphs/artin_wale_braids/Wale.py +67 -79
- knit_graphs/artin_wale_braids/Wale_Braid.py +8 -2
- knit_graphs/artin_wale_braids/Wale_Braid_Word.py +45 -31
- knit_graphs/artin_wale_braids/Wale_Group.py +53 -43
- knit_graphs/basic_knit_graph_generators.py +96 -140
- knit_graphs/directed_loop_graph.py +454 -0
- knit_graphs/knit_graph_builder.py +187 -0
- knit_graphs/knit_graph_errors/__init__.py +0 -0
- knit_graphs/knit_graph_errors/knit_graph_error.py +30 -0
- knit_graphs/py.typed +0 -0
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.12.dist-info}/METADATA +3 -2
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.12.dist-info}/RECORD +24 -19
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.12.dist-info}/LICENSE +0 -0
- {knit_graphs-0.0.10.dist-info → knit_graphs-0.0.12.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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__(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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[
|
|
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[
|
|
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:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
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 =
|
|
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(
|
|
215
|
-
|
|
216
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
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.
|
|
262
|
-
if (
|
|
263
|
-
|
|
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:
|
|
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
|
|
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[
|
|
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[
|
|
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 = {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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]] = {
|
|
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[
|
|
323
|
-
float_data[
|
|
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 = [
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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(
|
|
344
|
-
has_purls = any(
|
|
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
|
|
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(
|
|
354
|
-
|
|
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
|
|
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(
|
|
371
|
-
|
|
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(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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:
|
|
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:
|
|
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][
|
|
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:
|
|
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][
|
|
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:
|
|
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][
|
|
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:
|
|
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][
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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 = {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
500
|
-
|
|
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(
|
|
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
|
|
513
|
-
front_set
|
|
514
|
-
back_loops
|
|
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,
|
|
520
|
-
self._place_loop(
|
|
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 = [
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
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[
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
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:
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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(
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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(
|
|
654
|
-
|
|
655
|
-
|
|
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(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
)
|