pyforcedirectedlayout 2.0.0__tar.gz → 2.1.0__tar.gz
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.
- {pyforcedirectedlayout-2.0.0/src/pyforcedirectedlayout.egg-info → pyforcedirectedlayout-2.1.0}/PKG-INFO +10 -24
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/README.md +4 -22
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/pyproject.toml +7 -1
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Configuration.py +10 -9
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/ForceDirectedLayout.py +18 -17
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Node.py +8 -3
- pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/_version.py +1 -0
- pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/ConfigurationPanel.py +198 -0
- pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/DlgConfiguration.py +67 -0
- pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/__init__.py +1 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout.egg-info}/PKG-INFO +10 -24
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/SOURCES.txt +4 -1
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/requires.txt +6 -1
- pyforcedirectedlayout-2.0.0/src/pyforcedirectedlayout/_version.py +0 -1
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/LICENSE +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/setup.cfg +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/LayoutTypes.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/NodeLayoutInformation.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Point.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Rectangle.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Size.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Vector.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/__init__.py +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/py.typed +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/dependency_links.txt +0 -0
- {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyforcedirectedlayout
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python Implementation of a Force Directed Layout Algorithm
|
|
5
5
|
Author-email: "Humberto A. Sanchez II" <Humbert.A.Sanchez.II@gmail.com>
|
|
6
6
|
Maintainer-email: "Humberto A. Sanchez II" <Humbert.A.Sanchez.II@gmail.com>
|
|
@@ -18,15 +18,19 @@ Requires-Dist: wheel==0.46.3; extra == "deploy"
|
|
|
18
18
|
Requires-Dist: setuptools==82.0.1; extra == "deploy"
|
|
19
19
|
Requires-Dist: twine==6.2.0; extra == "deploy"
|
|
20
20
|
Requires-Dist: build==1.2.2.post1; extra == "deploy"
|
|
21
|
+
Provides-Extra: ui
|
|
22
|
+
Requires-Dist: wxPython>=4.3.1; extra == "ui"
|
|
23
|
+
Requires-Dist: codeallyadvanced>=2.5.8; extra == "ui"
|
|
21
24
|
Provides-Extra: demo
|
|
22
|
-
Requires-Dist: wxPython
|
|
25
|
+
Requires-Dist: wxPython>=4.3.1; extra == "demo"
|
|
26
|
+
Requires-Dist: codeallyadvanced>=2.5.8; extra == "demo"
|
|
23
27
|
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|

|
|
26
30
|
|
|
27
|
-
[](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
|
|
31
|
+
[](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
|
|
29
32
|
[](https://badge.fury.io/py/pyforcedirectedlayout)
|
|
33
|
+
[](https://github.com/hasii2011/py-force-directed-layout/graphs/commit-activity)
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
# Introduction
|
|
@@ -79,25 +83,7 @@ ___
|
|
|
79
83
|
|
|
80
84
|
# Note
|
|
81
85
|
For all kinds of problems, requests, enhancements, bug reports, etc., drop me an e-mail.
|
|
82
|
-
Written by <a href="mailto:
|
|
86
|
+
Written by <a href="mailto:humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> ©2026
|
|
83
87
|
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-

|
|
87
|
-
|
|
88
|
-
I am concerned about GitHub's Copilot project.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
I urge you to read about the
|
|
92
|
-
[Give up GitHub](https://GiveUpGitHub.org) campaign from
|
|
93
|
-
[the Software Freedom Conservancy](https://sfconservancy.org).
|
|
94
|
-
|
|
95
|
-
While I do not advocate for all the issues listed there, I do not like that
|
|
96
|
-
a company like Microsoft may profit from open source projects.
|
|
97
|
-
|
|
98
|
-
I continue to use GitHub because it offers the services I need for free. But I continue
|
|
99
|
-
to monitor their terms of service.
|
|
100
|
-
|
|
101
|
-
Any use of this project's code by GitHub Copilot, past or present, is done
|
|
102
|
-
without my permission. I do not consent to GitHub's use of this project's
|
|
103
|
-
code in Copilot.
|
|
89
|
+
[Copilot Statement](https://github.com/hasii2011/code-ally-basic/wiki/GitHub-Copilot).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
[](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
|
|
3
|
+
[](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
|
|
5
4
|
[](https://badge.fury.io/py/pyforcedirectedlayout)
|
|
5
|
+
[](https://github.com/hasii2011/py-force-directed-layout/graphs/commit-activity)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
# Introduction
|
|
@@ -55,25 +55,7 @@ ___
|
|
|
55
55
|
|
|
56
56
|
# Note
|
|
57
57
|
For all kinds of problems, requests, enhancements, bug reports, etc., drop me an e-mail.
|
|
58
|
-
Written by <a href="mailto:
|
|
58
|
+
Written by <a href="mailto:humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> ©2026
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-

|
|
63
|
-
|
|
64
|
-
I am concerned about GitHub's Copilot project.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
I urge you to read about the
|
|
68
|
-
[Give up GitHub](https://GiveUpGitHub.org) campaign from
|
|
69
|
-
[the Software Freedom Conservancy](https://sfconservancy.org).
|
|
70
|
-
|
|
71
|
-
While I do not advocate for all the issues listed there, I do not like that
|
|
72
|
-
a company like Microsoft may profit from open source projects.
|
|
73
|
-
|
|
74
|
-
I continue to use GitHub because it offers the services I need for free. But I continue
|
|
75
|
-
to monitor their terms of service.
|
|
76
|
-
|
|
77
|
-
Any use of this project's code by GitHub Copilot, past or present, is done
|
|
78
|
-
without my permission. I do not consent to GitHub's use of this project's
|
|
79
|
-
code in Copilot.
|
|
61
|
+
[Copilot Statement](https://github.com/hasii2011/code-ally-basic/wiki/GitHub-Copilot).
|
|
@@ -15,10 +15,10 @@ from codeallybasic.SecureConversions import SecureConversions
|
|
|
15
15
|
|
|
16
16
|
from codeallybasic.SingletonV3 import SingletonV3
|
|
17
17
|
|
|
18
|
-
X_RANGE_MIN: int =
|
|
19
|
-
X_RANGE_MAX: int =
|
|
20
|
-
Y_RANGE_MIN: int =
|
|
21
|
-
Y_RANGE_MAX: int =
|
|
18
|
+
X_RANGE_MIN: int = 0
|
|
19
|
+
X_RANGE_MAX: int = 2048
|
|
20
|
+
Y_RANGE_MIN: int = 0
|
|
21
|
+
Y_RANGE_MAX: int = 2048
|
|
22
22
|
|
|
23
23
|
arrangeProperties: ValueDescriptions = ValueDescriptions(
|
|
24
24
|
{
|
|
@@ -30,10 +30,10 @@ arrangeProperties: ValueDescriptions = ValueDescriptions(
|
|
|
30
30
|
}
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
DEFAULT_MIN_X_COORDINATE: int =
|
|
34
|
-
DEFAULT_MAX_X_COORDINATE: int =
|
|
35
|
-
DEFAULT_MIN_Y_COORDINATE: int =
|
|
36
|
-
DEFAULT_MAX_Y_COORDINATE: int =
|
|
33
|
+
DEFAULT_MIN_X_COORDINATE: int = 50
|
|
34
|
+
DEFAULT_MAX_X_COORDINATE: int = 500
|
|
35
|
+
DEFAULT_MIN_Y_COORDINATE: int = 50
|
|
36
|
+
DEFAULT_MAX_Y_COORDINATE: int = 500
|
|
37
37
|
|
|
38
38
|
DEFAULT_MIN_MAX_X: str = MinMax(minValue=DEFAULT_MIN_X_COORDINATE, maxValue=DEFAULT_MAX_X_COORDINATE).__repr__()
|
|
39
39
|
DEFAULT_MIN_MAX_Y: str = MinMax(minValue=DEFAULT_MIN_Y_COORDINATE, maxValue=DEFAULT_MAX_Y_COORDINATE).__repr__()
|
|
@@ -62,6 +62,7 @@ PYFDL_SECTIONS: Sections = Sections(
|
|
|
62
62
|
}
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
MODULE_NAME: str = 'pyfdl'
|
|
65
66
|
|
|
66
67
|
class Configuration(DynamicConfiguration, metaclass=SingletonV3):
|
|
67
68
|
|
|
@@ -69,4 +70,4 @@ class Configuration(DynamicConfiguration, metaclass=SingletonV3):
|
|
|
69
70
|
|
|
70
71
|
self._logger: Logger = getLogger(__name__)
|
|
71
72
|
|
|
72
|
-
super().__init__(baseFileName='
|
|
73
|
+
super().__init__(baseFileName=f'{MODULE_NAME}.ini', moduleName=f'{MODULE_NAME}', sections=PYFDL_SECTIONS)
|
|
@@ -16,6 +16,8 @@ from random import randint
|
|
|
16
16
|
from uuid import uuid4
|
|
17
17
|
from uuid import UUID
|
|
18
18
|
|
|
19
|
+
from codeallybasic.MinMax import MinMax
|
|
20
|
+
|
|
19
21
|
from pyforcedirectedlayout.Configuration import Configuration
|
|
20
22
|
from pyforcedirectedlayout.LayoutTypes import LayoutStatus
|
|
21
23
|
from pyforcedirectedlayout.LayoutTypes import LayoutStatusCallback
|
|
@@ -193,14 +195,14 @@ class ForceDirectedLayout:
|
|
|
193
195
|
|
|
194
196
|
def _adjustNodes(self):
|
|
195
197
|
logicalBounds: Rectangle = self._getDiagramBounds()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# )
|
|
199
|
-
midPoint: Point = Point(x=logicalBounds.x + logicalBounds.width,
|
|
200
|
-
y=logicalBounds.y + logicalBounds.height
|
|
198
|
+
midPoint: Point = Point(x=logicalBounds.x + (logicalBounds.width // 2),
|
|
199
|
+
y=logicalBounds.y + (logicalBounds.height // 2)
|
|
201
200
|
)
|
|
201
|
+
# midPoint: Point = Point(x=logicalBounds.x + logicalBounds.width,
|
|
202
|
+
# y=logicalBounds.y + logicalBounds.height
|
|
203
|
+
# )
|
|
202
204
|
for n in self._nodes:
|
|
203
|
-
node: Node =
|
|
205
|
+
node: Node = n
|
|
204
206
|
node.location -= midPoint
|
|
205
207
|
node.location.x += node.size.width
|
|
206
208
|
node.location.y += node.size.height
|
|
@@ -232,7 +234,7 @@ class ForceDirectedLayout:
|
|
|
232
234
|
netForce += self._calculateAttractionForce(x=currentLayoutNode, y=child, springLength=springLength)
|
|
233
235
|
|
|
234
236
|
for p in self._nodes:
|
|
235
|
-
parent: Node =
|
|
237
|
+
parent: Node = p
|
|
236
238
|
if currentLayoutNode in parent.connections:
|
|
237
239
|
netForce += self._calculateAttractionForce(x=currentLayoutNode, y=parent, springLength=springLength)
|
|
238
240
|
|
|
@@ -248,14 +250,12 @@ class ForceDirectedLayout:
|
|
|
248
250
|
layout: NodeLayoutInformationList = NodeLayoutInformationList([])
|
|
249
251
|
for node in self._nodes:
|
|
250
252
|
diagramNode: Node = node
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
diagramNode.location = Point(x=randint(-50, 50), y=randint(-50, 50))
|
|
258
|
-
# diagramNode.location = Point(x=randint(10, 100), y=randint(10, 100))
|
|
253
|
+
|
|
254
|
+
minMaxX: MinMax = self._configuration.minMaxX
|
|
255
|
+
minMaxY: MinMax = self._configuration.minMaxY
|
|
256
|
+
|
|
257
|
+
diagramNode.location = Point(x=randint(minMaxX.minValue, minMaxX.maxValue), y=randint(minMaxY.minValue, minMaxY.minValue))
|
|
258
|
+
|
|
259
259
|
layoutInformation: NodeLayoutInformation = NodeLayoutInformation(node=diagramNode,
|
|
260
260
|
velocity=Vector(magnitude=0, direction=0),
|
|
261
261
|
nextPosition=Point(),
|
|
@@ -396,9 +396,10 @@ class ForceDirectedLayout:
|
|
|
396
396
|
|
|
397
397
|
return int(distance)
|
|
398
398
|
|
|
399
|
-
def __eq__(self, other) -> bool:
|
|
399
|
+
def __eq__(self, other: object) -> bool:
|
|
400
400
|
|
|
401
401
|
if isinstance(other, ForceDirectedLayout) is False:
|
|
402
402
|
return False
|
|
403
403
|
|
|
404
|
-
|
|
404
|
+
otherMe: ForceDirectedLayout = cast(ForceDirectedLayout, other) # noqa
|
|
405
|
+
return self.id == otherMe.id
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Node.py
RENAMED
|
@@ -168,10 +168,15 @@ class Node(ABC):
|
|
|
168
168
|
else:
|
|
169
169
|
return True
|
|
170
170
|
|
|
171
|
-
def __eq__(self, other) -> bool:
|
|
171
|
+
def __eq__(self, other: object) -> bool:
|
|
172
172
|
|
|
173
|
-
ans:
|
|
174
|
-
|
|
173
|
+
ans: bool = False
|
|
174
|
+
realOther: Node = cast(Node, other)
|
|
175
|
+
|
|
176
|
+
if self.x == realOther.x and realOther.y == realOther.y and self.location == realOther.location:
|
|
175
177
|
ans = True
|
|
176
178
|
|
|
177
179
|
return ans
|
|
180
|
+
|
|
181
|
+
def __repr__(self) -> str:
|
|
182
|
+
return f'Node: location: {self.location})'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str = '2.1.0'
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
|
|
7
|
+
from wx import EVT_SPINCTRL
|
|
8
|
+
from wx import EVT_SPINCTRLDOUBLE
|
|
9
|
+
from wx import SP_VERTICAL
|
|
10
|
+
|
|
11
|
+
from wx import Size
|
|
12
|
+
from wx import SpinCtrl
|
|
13
|
+
from wx import SpinEvent
|
|
14
|
+
from wx import SpinCtrlDouble
|
|
15
|
+
from wx import DefaultPosition
|
|
16
|
+
from wx import SpinDoubleEvent
|
|
17
|
+
|
|
18
|
+
from wx.lib.sized_controls import SizedPanel
|
|
19
|
+
from wx.lib.sized_controls import SizedStaticBox
|
|
20
|
+
|
|
21
|
+
from codeallybasic.MinMax import MinMax
|
|
22
|
+
|
|
23
|
+
from codeallyadvanced.ui.widgets.DialSelector import DialSelector
|
|
24
|
+
from codeallyadvanced.ui.widgets.DialSelector import DialSelectorParameters
|
|
25
|
+
from codeallyadvanced.ui.widgets.MinMaxControl import MinMaxControl
|
|
26
|
+
|
|
27
|
+
from pyforcedirectedlayout.Configuration import Configuration
|
|
28
|
+
from pyforcedirectedlayout.Configuration import X_RANGE_MAX
|
|
29
|
+
from pyforcedirectedlayout.Configuration import X_RANGE_MIN
|
|
30
|
+
from pyforcedirectedlayout.Configuration import Y_RANGE_MAX
|
|
31
|
+
from pyforcedirectedlayout.Configuration import Y_RANGE_MIN
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
NODE_ATTRACTION_FORCE_MIN: float = 0.01
|
|
35
|
+
NODE_ATTRACTION_FORCE_MAX: float = 1.0
|
|
36
|
+
NODE_ATTRACTION_FORCE_INCREMENT: float = 0.05
|
|
37
|
+
|
|
38
|
+
REPULSION_FORCE_MIN: int = 500
|
|
39
|
+
REPULSION_FORCE_MAX: int = 25000
|
|
40
|
+
|
|
41
|
+
NO_DIAL_SELECTOR: DialSelector = cast(DialSelector, cast(object, None))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigurationPanel:
|
|
45
|
+
"""
|
|
46
|
+
Builds the force-directed layout configuration controls inside a parent SizedPanel.
|
|
47
|
+
Assumes the parent panel is vertically oriented.
|
|
48
|
+
|
|
49
|
+
Updates the Configuration singleton as the user changes values.
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, sizedPanel: SizedPanel):
|
|
52
|
+
|
|
53
|
+
self.logger: Logger = getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
self._configuration: Configuration = Configuration()
|
|
56
|
+
|
|
57
|
+
self._damping: DialSelector = NO_DIAL_SELECTOR
|
|
58
|
+
self._springLength: DialSelector = NO_DIAL_SELECTOR
|
|
59
|
+
self._maxIterations: DialSelector = NO_DIAL_SELECTOR
|
|
60
|
+
|
|
61
|
+
self._layoutForceParameters(parentPanel=sizedPanel)
|
|
62
|
+
self._layoutRandomizeParameters(parentPanel=sizedPanel)
|
|
63
|
+
self._layoutAlgorithmParameters(parentPanel=sizedPanel)
|
|
64
|
+
|
|
65
|
+
def _layoutForceParameters(self, parentPanel: SizedPanel):
|
|
66
|
+
"""
|
|
67
|
+
Sets the protected properties:
|
|
68
|
+
|
|
69
|
+
self._damping
|
|
70
|
+
self._springLength
|
|
71
|
+
self._maxIterations
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
parentPanel: The panel that hosts these components
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
localPanel: SizedStaticBox = SizedStaticBox(parentPanel, label='Directed Layout Parameters')
|
|
78
|
+
localPanel.SetSizerType('horizontal')
|
|
79
|
+
localPanel.SetSizerProps(expand=True, proportion=2)
|
|
80
|
+
|
|
81
|
+
dampingParameters: DialSelectorParameters = DialSelectorParameters(minValue=0.1, maxValue=1.0, dialLabel='Damping',
|
|
82
|
+
formatValueCallback=self._formatDampingValue,
|
|
83
|
+
valueChangedCallback=self._dampingChanged)
|
|
84
|
+
damping: DialSelector = DialSelector(localPanel, parameters=dampingParameters)
|
|
85
|
+
damping.tickFrequency = 10
|
|
86
|
+
damping.tickValue = 0.1
|
|
87
|
+
damping.value = self._configuration.damping
|
|
88
|
+
|
|
89
|
+
springLengthParameters: DialSelectorParameters = DialSelectorParameters(minValue=100, maxValue=500, dialLabel='Spring Length',
|
|
90
|
+
formatValueCallback=self._formatSpringLength,
|
|
91
|
+
valueChangedCallback=self._springLengthChanged)
|
|
92
|
+
|
|
93
|
+
springLength: DialSelector = DialSelector(localPanel, parameters=springLengthParameters)
|
|
94
|
+
springLength.tickFrequency = 20
|
|
95
|
+
springLength.tickValue = 25
|
|
96
|
+
springLength.value = self._configuration.springLength
|
|
97
|
+
|
|
98
|
+
maxIterationsParameters: DialSelectorParameters = DialSelectorParameters(minValue=100,
|
|
99
|
+
maxValue=1000,
|
|
100
|
+
dialLabel='Maximum Iterations',
|
|
101
|
+
formatValueCallback=self._formatMaxIterations,
|
|
102
|
+
valueChangedCallback=self._maxIterationsChanged)
|
|
103
|
+
maxIterations: DialSelector = DialSelector(localPanel, parameters=maxIterationsParameters)
|
|
104
|
+
maxIterations.tickFrequency = 50
|
|
105
|
+
maxIterations.tickValue = 20
|
|
106
|
+
maxIterations.value = self._configuration.maxIterations
|
|
107
|
+
|
|
108
|
+
self._damping = damping
|
|
109
|
+
self._springLength = springLength
|
|
110
|
+
self._maxIterations = maxIterations
|
|
111
|
+
|
|
112
|
+
def _layoutRandomizeParameters(self, parentPanel: SizedPanel):
|
|
113
|
+
|
|
114
|
+
localPanel: SizedStaticBox = SizedStaticBox(parentPanel, label='Randomize Initial Layout Parameters')
|
|
115
|
+
localPanel.SetSizerType('vertical')
|
|
116
|
+
localPanel.SetSizerProps(expand=True, proportion=1)
|
|
117
|
+
|
|
118
|
+
horizontalPanel: SizedPanel = SizedPanel(localPanel)
|
|
119
|
+
horizontalPanel.SetSizerType('horizontal')
|
|
120
|
+
horizontalPanel.SetSizerProps(expand=True, proportion=1)
|
|
121
|
+
|
|
122
|
+
minMaxX: MinMaxControl = MinMaxControl(sizedPanel=horizontalPanel, displayText='Minimum/Maximum X Value',
|
|
123
|
+
minValue=X_RANGE_MIN, maxValue=X_RANGE_MAX,
|
|
124
|
+
valueChangedCallback=self._onMinMaxX,
|
|
125
|
+
setControlsSize=False)
|
|
126
|
+
minMaxX.minMax = self._configuration.minMaxX
|
|
127
|
+
|
|
128
|
+
minMaxY: MinMaxControl = MinMaxControl(sizedPanel=horizontalPanel, displayText='Minimum/Maximum Y Value',
|
|
129
|
+
minValue=Y_RANGE_MIN, maxValue=Y_RANGE_MAX,
|
|
130
|
+
valueChangedCallback=self._onMinMaxY,
|
|
131
|
+
setControlsSize=False)
|
|
132
|
+
minMaxY.minMax = self._configuration.minMaxY
|
|
133
|
+
|
|
134
|
+
def _layoutAlgorithmParameters(self, parentPanel: SizedPanel):
|
|
135
|
+
|
|
136
|
+
algorithmFactorsPanel: SizedStaticBox = SizedStaticBox(parentPanel, label='Algorithm Parameters')
|
|
137
|
+
algorithmFactorsPanel.SetSizerType('horizontal')
|
|
138
|
+
algorithmFactorsPanel.SetSizerProps(expand=True, proportion=1)
|
|
139
|
+
|
|
140
|
+
attractionPanel: SizedStaticBox = SizedStaticBox(algorithmFactorsPanel, label='Node Attraction Force')
|
|
141
|
+
attractionPanel.SetSizerType('vertical')
|
|
142
|
+
attractionPanel.SetSizerProps(proportion=0)
|
|
143
|
+
|
|
144
|
+
attractionForce: SpinCtrlDouble = SpinCtrlDouble(attractionPanel,
|
|
145
|
+
min=NODE_ATTRACTION_FORCE_MIN,
|
|
146
|
+
max=NODE_ATTRACTION_FORCE_MAX,
|
|
147
|
+
inc=NODE_ATTRACTION_FORCE_INCREMENT,
|
|
148
|
+
size=Size(75, 35))
|
|
149
|
+
attractionForce.SetDigits(2)
|
|
150
|
+
attractionForce.SetValue(self._configuration.attractionForce)
|
|
151
|
+
attractionForce.Bind(EVT_SPINCTRLDOUBLE, self._attractionForceChanged)
|
|
152
|
+
|
|
153
|
+
repulsionPanel: SizedStaticBox = SizedStaticBox(algorithmFactorsPanel, label='Node Repulsion Force')
|
|
154
|
+
repulsionPanel.SetSizerType('vertical')
|
|
155
|
+
repulsionPanel.SetSizerProps(proportion=0)
|
|
156
|
+
|
|
157
|
+
repulsionForce: SpinCtrl = SpinCtrl(repulsionPanel, size=Size(75, 35), pos=DefaultPosition, style=SP_VERTICAL)
|
|
158
|
+
repulsionForce.SetRange(REPULSION_FORCE_MIN, REPULSION_FORCE_MAX)
|
|
159
|
+
repulsionForce.SetValue(self._configuration.repulsionForce)
|
|
160
|
+
repulsionForce.SetIncrement(100)
|
|
161
|
+
repulsionForce.Bind(EVT_SPINCTRL, self._repulsionForceChanged)
|
|
162
|
+
|
|
163
|
+
def _formatDampingValue(self, valueToFormat: float):
|
|
164
|
+
|
|
165
|
+
return f'{valueToFormat:.2f}'
|
|
166
|
+
|
|
167
|
+
def _formatSpringLength(self, valueToFormat: int):
|
|
168
|
+
return f'{valueToFormat}'
|
|
169
|
+
|
|
170
|
+
def _formatMaxIterations(self, valueToFormat: int):
|
|
171
|
+
return f'{valueToFormat}'
|
|
172
|
+
|
|
173
|
+
def _dampingChanged(self, newValue: int):
|
|
174
|
+
self._configuration.damping = newValue
|
|
175
|
+
|
|
176
|
+
def _springLengthChanged(self, newValue: int):
|
|
177
|
+
self._configuration.springLength = newValue
|
|
178
|
+
|
|
179
|
+
def _maxIterationsChanged(self, newValue: int):
|
|
180
|
+
self._configuration.maxIterations = newValue
|
|
181
|
+
|
|
182
|
+
def _onMinMaxX(self, minMaxX: MinMax):
|
|
183
|
+
self._configuration.minMaxX = minMaxX
|
|
184
|
+
|
|
185
|
+
def _onMinMaxY(self, minMaxY: MinMax):
|
|
186
|
+
self._configuration.minMaxY = minMaxY
|
|
187
|
+
|
|
188
|
+
def _attractionForceChanged(self, event: SpinDoubleEvent):
|
|
189
|
+
|
|
190
|
+
spinCtrlDouble: SpinCtrlDouble = cast(SpinCtrlDouble, event.GetEventObject())
|
|
191
|
+
|
|
192
|
+
self._configuration.attractionForce = spinCtrlDouble.GetValue()
|
|
193
|
+
|
|
194
|
+
def _repulsionForceChanged(self, event: SpinEvent):
|
|
195
|
+
|
|
196
|
+
spinCtrl: SpinCtrl = cast(SpinCtrl, event.GetEventObject())
|
|
197
|
+
|
|
198
|
+
self._configuration.repulsionForce = spinCtrl.GetValue()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
|
|
5
|
+
from wx import OK
|
|
6
|
+
from wx import ID_OK
|
|
7
|
+
from wx import CANCEL
|
|
8
|
+
from wx import EVT_CLOSE
|
|
9
|
+
from wx import EVT_BUTTON
|
|
10
|
+
from wx import ID_CANCEL
|
|
11
|
+
from wx import DEFAULT_DIALOG_STYLE
|
|
12
|
+
|
|
13
|
+
from wx import Size
|
|
14
|
+
from wx import Window
|
|
15
|
+
from wx import CommandEvent
|
|
16
|
+
from wx import StdDialogButtonSizer
|
|
17
|
+
|
|
18
|
+
from wx.lib.sized_controls import SizedDialog
|
|
19
|
+
from wx.lib.sized_controls import SizedPanel
|
|
20
|
+
|
|
21
|
+
from pyforcedirectedlayout.ui.ConfigurationPanel import ConfigurationPanel
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DlgConfiguration(SizedDialog):
|
|
25
|
+
"""
|
|
26
|
+
A reusable dialog wrapping ConfigurationPanel with standard OK/Cancel buttons.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent: Window):
|
|
30
|
+
|
|
31
|
+
style: int = DEFAULT_DIALOG_STYLE
|
|
32
|
+
dlgSize: Size = Size(470, 540)
|
|
33
|
+
|
|
34
|
+
super().__init__(parent, title='Force Directed Configuration', size=dlgSize, style=style)
|
|
35
|
+
|
|
36
|
+
self.logger: Logger = getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
sizedPanel: SizedPanel = self.GetContentsPane()
|
|
39
|
+
sizedPanel.SetSizerType('vertical')
|
|
40
|
+
sizedPanel.SetSizerProps(proportion=1)
|
|
41
|
+
|
|
42
|
+
self._configurationPanel: ConfigurationPanel = ConfigurationPanel(sizedPanel=sizedPanel)
|
|
43
|
+
self._layoutStandardOkCancelButtonSizer()
|
|
44
|
+
|
|
45
|
+
def _layoutStandardOkCancelButtonSizer(self):
|
|
46
|
+
"""
|
|
47
|
+
Call this last when creating controls; Will take care of
|
|
48
|
+
adding callbacks for the Ok and Cancel buttons
|
|
49
|
+
"""
|
|
50
|
+
buttSizer: StdDialogButtonSizer = self.CreateStdDialogButtonSizer(OK | CANCEL)
|
|
51
|
+
|
|
52
|
+
self.SetButtonSizer(buttSizer)
|
|
53
|
+
self.Bind(EVT_BUTTON, self._onOk, id=ID_OK)
|
|
54
|
+
self.Bind(EVT_BUTTON, self._onClose, id=ID_CANCEL)
|
|
55
|
+
self.Bind(EVT_CLOSE, self._onClose)
|
|
56
|
+
|
|
57
|
+
# noinspection PyUnusedLocal
|
|
58
|
+
def _onOk(self, _event: CommandEvent):
|
|
59
|
+
"""
|
|
60
|
+
"""
|
|
61
|
+
self.EndModal(OK)
|
|
62
|
+
|
|
63
|
+
# noinspection PyUnusedLocal
|
|
64
|
+
def _onClose(self, _event: CommandEvent):
|
|
65
|
+
"""
|
|
66
|
+
"""
|
|
67
|
+
self.EndModal(CANCEL)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyforcedirectedlayout
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python Implementation of a Force Directed Layout Algorithm
|
|
5
5
|
Author-email: "Humberto A. Sanchez II" <Humbert.A.Sanchez.II@gmail.com>
|
|
6
6
|
Maintainer-email: "Humberto A. Sanchez II" <Humbert.A.Sanchez.II@gmail.com>
|
|
@@ -18,15 +18,19 @@ Requires-Dist: wheel==0.46.3; extra == "deploy"
|
|
|
18
18
|
Requires-Dist: setuptools==82.0.1; extra == "deploy"
|
|
19
19
|
Requires-Dist: twine==6.2.0; extra == "deploy"
|
|
20
20
|
Requires-Dist: build==1.2.2.post1; extra == "deploy"
|
|
21
|
+
Provides-Extra: ui
|
|
22
|
+
Requires-Dist: wxPython>=4.3.1; extra == "ui"
|
|
23
|
+
Requires-Dist: codeallyadvanced>=2.5.8; extra == "ui"
|
|
21
24
|
Provides-Extra: demo
|
|
22
|
-
Requires-Dist: wxPython
|
|
25
|
+
Requires-Dist: wxPython>=4.3.1; extra == "demo"
|
|
26
|
+
Requires-Dist: codeallyadvanced>=2.5.8; extra == "demo"
|
|
23
27
|
Dynamic: license-file
|
|
24
28
|
|
|
25
29
|

|
|
26
30
|
|
|
27
|
-
[](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
|
|
31
|
+
[](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
|
|
29
32
|
[](https://badge.fury.io/py/pyforcedirectedlayout)
|
|
33
|
+
[](https://github.com/hasii2011/py-force-directed-layout/graphs/commit-activity)
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
# Introduction
|
|
@@ -79,25 +83,7 @@ ___
|
|
|
79
83
|
|
|
80
84
|
# Note
|
|
81
85
|
For all kinds of problems, requests, enhancements, bug reports, etc., drop me an e-mail.
|
|
82
|
-
Written by <a href="mailto:
|
|
86
|
+
Written by <a href="mailto:humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> ©2026
|
|
83
87
|
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-

|
|
87
|
-
|
|
88
|
-
I am concerned about GitHub's Copilot project.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
I urge you to read about the
|
|
92
|
-
[Give up GitHub](https://GiveUpGitHub.org) campaign from
|
|
93
|
-
[the Software Freedom Conservancy](https://sfconservancy.org).
|
|
94
|
-
|
|
95
|
-
While I do not advocate for all the issues listed there, I do not like that
|
|
96
|
-
a company like Microsoft may profit from open source projects.
|
|
97
|
-
|
|
98
|
-
I continue to use GitHub because it offers the services I need for free. But I continue
|
|
99
|
-
to monitor their terms of service.
|
|
100
|
-
|
|
101
|
-
Any use of this project's code by GitHub Copilot, past or present, is done
|
|
102
|
-
without my permission. I do not consent to GitHub's use of this project's
|
|
103
|
-
code in Copilot.
|
|
89
|
+
[Copilot Statement](https://github.com/hasii2011/code-ally-basic/wiki/GitHub-Copilot).
|
|
@@ -17,4 +17,7 @@ src/pyforcedirectedlayout.egg-info/PKG-INFO
|
|
|
17
17
|
src/pyforcedirectedlayout.egg-info/SOURCES.txt
|
|
18
18
|
src/pyforcedirectedlayout.egg-info/dependency_links.txt
|
|
19
19
|
src/pyforcedirectedlayout.egg-info/requires.txt
|
|
20
|
-
src/pyforcedirectedlayout.egg-info/top_level.txt
|
|
20
|
+
src/pyforcedirectedlayout.egg-info/top_level.txt
|
|
21
|
+
src/pyforcedirectedlayout/ui/ConfigurationPanel.py
|
|
22
|
+
src/pyforcedirectedlayout/ui/DlgConfiguration.py
|
|
23
|
+
src/pyforcedirectedlayout/ui/__init__.py
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
codeallybasic>=1.30.0
|
|
2
2
|
|
|
3
3
|
[demo]
|
|
4
|
-
wxPython
|
|
4
|
+
wxPython>=4.3.1
|
|
5
|
+
codeallyadvanced>=2.5.8
|
|
5
6
|
|
|
6
7
|
[deploy]
|
|
7
8
|
wheel==0.46.3
|
|
@@ -12,3 +13,7 @@ build==1.2.2.post1
|
|
|
12
13
|
[test]
|
|
13
14
|
mypy==1.19.1
|
|
14
15
|
html-testRunner~=1.2.1
|
|
16
|
+
|
|
17
|
+
[ui]
|
|
18
|
+
wxPython>=4.3.1
|
|
19
|
+
codeallyadvanced>=2.5.8
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__: str = '2.0.0'
|
|
File without changes
|
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/LayoutTypes.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Point.py
RENAMED
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Rectangle.py
RENAMED
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Size.py
RENAMED
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Vector.py
RENAMED
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/__init__.py
RENAMED
|
File without changes
|
{pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|