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.
Files changed (26) hide show
  1. {pyforcedirectedlayout-2.0.0/src/pyforcedirectedlayout.egg-info → pyforcedirectedlayout-2.1.0}/PKG-INFO +10 -24
  2. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/README.md +4 -22
  3. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/pyproject.toml +7 -1
  4. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Configuration.py +10 -9
  5. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/ForceDirectedLayout.py +18 -17
  6. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Node.py +8 -3
  7. pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/_version.py +1 -0
  8. pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/ConfigurationPanel.py +198 -0
  9. pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/DlgConfiguration.py +67 -0
  10. pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout/ui/__init__.py +1 -0
  11. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0/src/pyforcedirectedlayout.egg-info}/PKG-INFO +10 -24
  12. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/SOURCES.txt +4 -1
  13. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/requires.txt +6 -1
  14. pyforcedirectedlayout-2.0.0/src/pyforcedirectedlayout/_version.py +0 -1
  15. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/LICENSE +0 -0
  16. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/setup.cfg +0 -0
  17. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/LayoutTypes.py +0 -0
  18. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/NodeLayoutInformation.py +0 -0
  19. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Point.py +0 -0
  20. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Rectangle.py +0 -0
  21. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Size.py +0 -0
  22. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/Vector.py +0 -0
  23. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/__init__.py +0 -0
  24. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout/py.typed +0 -0
  25. {pyforcedirectedlayout-2.0.0 → pyforcedirectedlayout-2.1.0}/src/pyforcedirectedlayout.egg-info/dependency_links.txt +0 -0
  26. {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.0.0
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==4.2.4; extra == "demo"
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
  ![](https://github.com/hasii2011/code-ally-basic/blob/master/developer/agpl-license-web-badge-version-2-256x48.png "AGPL")
26
30
 
27
- [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
28
- [![CircleCI](https://dl.circleci.com/status-badge/img/gh/hasii2011/py-force-directed-layout/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
31
+ [![CI](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
29
32
  [![PyPI version](https://badge.fury.io/py/pyforcedirectedlayout.svg)](https://badge.fury.io/py/pyforcedirectedlayout)
33
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](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:email@humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> (C) 2024
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
- ![Humberto's Modified Logo](https://raw.githubusercontent.com/wiki/hasii2011/gittodoistclone/images/SillyGitHub.png)
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
  ![](https://github.com/hasii2011/code-ally-basic/blob/master/developer/agpl-license-web-badge-version-2-256x48.png "AGPL")
2
2
 
3
- [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
4
- [![CircleCI](https://dl.circleci.com/status-badge/img/gh/hasii2011/py-force-directed-layout/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
3
+ [![CI](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
5
4
  [![PyPI version](https://badge.fury.io/py/pyforcedirectedlayout.svg)](https://badge.fury.io/py/pyforcedirectedlayout)
5
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](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:email@humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> (C) 2024
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
- ![Humberto's Modified Logo](https://raw.githubusercontent.com/wiki/hasii2011/gittodoistclone/images/SillyGitHub.png)
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).
@@ -29,8 +29,14 @@ deploy = [
29
29
  'build==1.2.2.post1',
30
30
  ]
31
31
 
32
+ ui = [
33
+ 'wxPython>=4.3.1',
34
+ 'codeallyadvanced>=2.5.8',
35
+ ]
36
+
32
37
  demo = [
33
- 'wxPython==4.2.4',
38
+ 'wxPython>=4.3.1',
39
+ 'codeallyadvanced>=2.5.8',
34
40
  ]
35
41
 
36
42
  [project.urls]
@@ -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 = -1024
19
- X_RANGE_MAX: int = 1024
20
- Y_RANGE_MIN: int = -1024
21
- Y_RANGE_MAX: int = 1024
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 = -50
34
- DEFAULT_MAX_X_COORDINATE: int = 50
35
- DEFAULT_MIN_Y_COORDINATE: int = -50
36
- DEFAULT_MAX_Y_COORDINATE: int = 50
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='pyfdl.ini', moduleName='pydfl', sections=PYFDL_SECTIONS)
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
- # midPoint: Point = Point(x=logicalBounds.x + (logicalBounds.width // 2),
197
- # y=logicalBounds.y + (logicalBounds.height // 2)
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 = cast(Node, n)
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 = cast(Node, p)
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
- # minRandomX: int = self._configuration.minPoint.x
252
- # maxRandomX: int = self._configuration.maxPoint.x
253
- # minRandomY: int = self._configuration.minPoint.y
254
- # maxRandomY: int = self._configuration.maxPoint.y
255
-
256
- # diagramNode.location = Point(x=randint(minRandomX, maxRandomX), y=randint(minRandomY, maxRandomY))
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
- return self.id == other.id
404
+ otherMe: ForceDirectedLayout = cast(ForceDirectedLayout, other) # noqa
405
+ return self.id == otherMe.id
@@ -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: bool = False
174
- if self.x == other.x and self.y == other .y and self.location == other.location:
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyforcedirectedlayout
3
- Version: 2.0.0
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==4.2.4; extra == "demo"
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
  ![](https://github.com/hasii2011/code-ally-basic/blob/master/developer/agpl-license-web-badge-version-2-256x48.png "AGPL")
26
30
 
27
- [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
28
- [![CircleCI](https://dl.circleci.com/status-badge/img/gh/hasii2011/py-force-directed-layout/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/hasii2011/py-force-directed-layout/tree/master)
31
+ [![CI](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hasii2011/py-force-directed-layout/actions/workflows/ci.yml)
29
32
  [![PyPI version](https://badge.fury.io/py/pyforcedirectedlayout.svg)](https://badge.fury.io/py/pyforcedirectedlayout)
33
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](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:email@humberto.a.sanchez.ii@gmail.com?subject=Hello Humberto">Humberto A. Sanchez II</a> (C) 2024
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
- ![Humberto's Modified Logo](https://raw.githubusercontent.com/wiki/hasii2011/gittodoistclone/images/SillyGitHub.png)
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.2.4
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'