├── freecad
└── plot
│ ├── __init__.py
│ ├── Resources
│ ├── Locales
│ │ └── Plot_ja.qm
│ ├── Interface
│ │ ├── Positions.ui
│ │ ├── Labels.ui
│ │ ├── Save.ui
│ │ ├── Series.ui
│ │ └── Axes.ui
│ └── Icons
│ │ ├── Series.svg
│ │ ├── Axes.svg
│ │ ├── Addon.svg
│ │ ├── Grid.svg
│ │ ├── Save.svg
│ │ └── Labels.svg
│ ├── PySide
│ ├── QtGui.py
│ ├── QtCore.py
│ └── QtWidgets.py
│ ├── Commands
│ ├── __init__.py
│ ├── Axes.py
│ ├── Save.py
│ ├── Labels.py
│ ├── Series.py
│ ├── Positions.py
│ ├── Grid.py
│ └── Legend.py
│ ├── init_gui.py
│ ├── Panels
│ ├── __init__.py
│ ├── Save.py
│ ├── Labels.py
│ ├── Positions.py
│ ├── Series.py
│ └── Axes.py
│ ├── MatPlot.py
│ ├── Toolbar.py
│ ├── Workbench.py
│ └── Backend.py
├── .pre-commit-config.yaml
├── Resources
├── Images
│ └── Preview-Toolbar.webp
└── Locales
│ └── ja.ts
├── pyproject.toml
├── .vscode
└── settings.json
├── .config
└── Tasks
│ ├── Release-Locales.sh
│ └── Update-Locales.sh
├── .gitignore
├── README.md
├── package.xml
└── .editorconfig
/freecad/plot/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | # Stub file to shut the pre-commit hook up!
--------------------------------------------------------------------------------
/Resources/Images/Preview-Toolbar.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeCAD/Plot/HEAD/Resources/Images/Preview-Toolbar.webp
--------------------------------------------------------------------------------
/freecad/plot/Resources/Locales/Plot_ja.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeCAD/Plot/HEAD/freecad/plot/Resources/Locales/Plot_ja.qm
--------------------------------------------------------------------------------
/freecad/plot/PySide/QtGui.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import TYPE_CHECKING
3 |
4 |
5 | if TYPE_CHECKING:
6 | from PySide6.QtGui import *
7 | else:
8 | from PySide.QtGui import *
--------------------------------------------------------------------------------
/freecad/plot/PySide/QtCore.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import TYPE_CHECKING
3 |
4 |
5 | if TYPE_CHECKING:
6 | from PySide6.QtCore import *
7 | else:
8 | from PySide.QtCore import *
--------------------------------------------------------------------------------
/freecad/plot/PySide/QtWidgets.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import TYPE_CHECKING
3 |
4 |
5 | if TYPE_CHECKING:
6 | from PySide6.QtWidgets import *
7 | else:
8 | from PySide.QtWidgets import *
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 |
2 | [project]
3 | version = '0.0.0'
4 | name = 'Plot'
5 |
6 | requires-python = '>=3.10'
7 |
8 | dependencies = [
9 | 'freecad-stubs>=1.0.21' ,
10 | 'matplotlib>=3.10.7' ,
11 | 'pyside6>=6.9.2'
12 | ]
--------------------------------------------------------------------------------
/freecad/plot/Commands/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .Positions import Positions
3 | from .Labels import Labels
4 | from .Series import Series
5 | from .Legend import Legend
6 | from .Axes import Axes
7 | from .Grid import Grid
8 | from .Save import Save
9 |
--------------------------------------------------------------------------------
/freecad/plot/init_gui.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from .Workbench import PlotWorkbench
4 | from .MatPlot import initMatPlot
5 | from FreeCAD import Gui
6 |
7 |
8 | initMatPlot()
9 |
10 | Gui.addWorkbench(PlotWorkbench())
11 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .Positions import createTask as createPositions
3 | from .Labels import createTask as createLabels
4 | from .Series import createTask as createSeries
5 | from .Save import createTask as createSave
6 | from .Axes import createTask as createAxes
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "qtForPython.uic.liveExecution.enabled" : false ,
3 |
4 | "files.associations" : {
5 | "**/Resources/Locales/*.ts": "xml",
6 | "LICENSE-*" : "txt"
7 | },
8 |
9 | "search.exclude" : {
10 | "uv.lock" : true
11 | }
12 | }
--------------------------------------------------------------------------------
/.config/Tasks/Release-Locales.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 |
6 | release='/usr/lib/qt6/bin/lrelease'
7 |
8 | output='freecad/plot/Resources/Locales'
9 | input='Resources/Locales'
10 |
11 |
12 | releaseLocale (){
13 |
14 | local file=$1
15 |
16 | local source="${input}/${file}.ts"
17 | local target="${output}/Plot_${file}.qm"
18 |
19 | "$release" \
20 | -nounfinished \
21 | "${source}" \
22 | -qm "${target}"
23 | }
24 |
25 |
26 | releaseLocale 'ja'
27 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Axes.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from ..Panels import createAxes
4 | from FreeCAD import Qt
5 |
6 |
7 | translate = Qt.translate
8 |
9 | Tooltip = translate('Plot_Axes','Configure the axes parameters')
10 | Title = translate('Plot_Axes','Configure axes')
11 |
12 |
13 | class Axes :
14 |
15 | def GetResources ( self ):
16 | return {
17 | 'MenuText' : Title ,
18 | 'ToolTip' : Tooltip ,
19 | 'Pixmap' : 'Axes'
20 | }
21 |
22 | def Activated ( self ):
23 | createAxes()
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Save.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from ..Panels import createSave
5 | from FreeCAD import Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Tooltip = translate('Plot_SaveFig','Save the plot as an image file')
11 | Title = translate('Plot_SaveFig','Save plot')
12 |
13 |
14 | class Save :
15 |
16 | def GetResources ( self ):
17 | return {
18 | 'MenuText' : Title ,
19 | 'ToolTip' : Tooltip ,
20 | 'Pixmap' : 'Save'
21 | }
22 |
23 | def Activated ( self ):
24 | createSave()
--------------------------------------------------------------------------------
/.config/Tasks/Update-Locales.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 |
6 | update='/usr/lib/qt6/bin/lupdate'
7 |
8 | output='Resources/Locales'
9 |
10 |
11 | files=($( find "freecad" -name "*.ui" -o -name "*.py" ))
12 |
13 |
14 | updateLocale (){
15 |
16 | local locale=$1
17 |
18 | local file="${output}/${locale}.ts"
19 |
20 | "$update" "${files[@]}" \
21 | -source-language en_US \
22 | -target-language "${locale}" \
23 | -no-obsolete \
24 | -ts "${file}"
25 | }
26 |
27 |
28 | updateLocale 'ja'
29 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Labels.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from ..Panels import createLabels
5 | from FreeCAD import Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Tooltip = translate('Plot_Labels','Set title and axes labels')
11 | Title = translate('Plot_Labels','Set labels')
12 |
13 |
14 | class Labels :
15 |
16 | def GetResources ( self ):
17 | return {
18 | 'MenuText' : Title ,
19 | 'ToolTip' : Tooltip ,
20 | 'Pixmap' : 'Labels'
21 | }
22 |
23 | def Activated ( self ):
24 | createLabels()
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Series.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from ..Panels import createSeries
5 | from FreeCAD import Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Tooltip = translate('Plot_Series','Configure series drawing style and label')
11 | Title = translate('Plot_Series','Configure series')
12 |
13 |
14 | class Series :
15 |
16 | def GetResources ( self ):
17 | return {
18 | 'MenuText' : Title ,
19 | 'ToolTip' : Tooltip ,
20 | 'Pixmap' : 'Series'
21 | }
22 |
23 | def Activated ( self ):
24 | createSeries()
25 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Positions.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from ..Panels import createPositions
5 | from FreeCAD import Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Tooltip = translate('Plot_Positions','Set labels and legend positions and sizes')
11 | Title = translate('Plot_Positions','Set positions and sizes')
12 |
13 |
14 | class Positions :
15 |
16 | def GetResources ( self ):
17 | return {
18 | 'MenuText' : Title ,
19 | 'ToolTip' : Tooltip ,
20 | 'Pixmap' : 'Positions'
21 | }
22 |
23 | def Activated ( self ):
24 | createPositions()
--------------------------------------------------------------------------------
/freecad/plot/Commands/Grid.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from FreeCAD import Console , Qt
5 |
6 |
7 | translate = Qt.translate
8 |
9 | Tooltip = translate('Plot_Grid','Show/Hide grid on selected plot')
10 | Title = translate('Plot_Grid','Show/Hide grid')
11 |
12 |
13 | class Grid :
14 |
15 | def GetResources ( self ):
16 | return {
17 | 'MenuText' : Title ,
18 | 'ToolTip' : Tooltip ,
19 | 'Pixmap' : 'Grid'
20 | }
21 |
22 | def Activated ( self ):
23 |
24 | plot = Plot.getPlot()
25 |
26 | if plot :
27 |
28 | isGrid = plot.isGrid()
29 | Plot.grid(not isGrid)
30 | return
31 |
32 | message = Qt.translate(
33 | 'plot_console',
34 | 'The grid must be activated on top of a plot document'
35 | )
36 |
37 | Console.PrintError(f'{ message }\n')
38 |
--------------------------------------------------------------------------------
/freecad/plot/MatPlot.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from matplotlib.pyplot import style , ion
4 | from matplotlib import rcParams , use
5 | from FreeCAD import Console , Qt
6 |
7 |
8 | def initMatPlot ():
9 |
10 | use('module://freecad.plot.Backend')
11 |
12 |
13 | style_list = [ 'default' , 'classic' ] + sorted(
14 | style for style in style.available
15 | if style != 'classic' and not style.startswith('_') and 'colorblind' in style
16 | )
17 |
18 | sorted_style_list = sorted(style_list,reverse = True)
19 |
20 | if len(sorted_style_list) > 1:
21 | style.use(sorted_style_list[ 1 ])
22 | elif len(sorted_style_list) == 1:
23 | style.use(sorted_style_list[ 0 ])
24 | else:
25 | Console.PrintWarning(
26 | Qt.translate('plot_console', 'matplotlib style sheets not found') + '\n'
27 | )
28 |
29 | rcParams[ 'figure.facecolor' ] = 'efefef'
30 | rcParams[ 'axes.facecolor' ] = 'efefef'
31 |
32 | ion()
33 |
--------------------------------------------------------------------------------
/freecad/plot/Commands/Legend.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from FreeCAD.Plot import Plot # type: ignore
5 | from FreeCAD import Console , Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Tooltip = translate('Plot_Legend','Show/Hide legend on selected plot')
11 | Title = translate('Plot_Legend','Show/Hide legend')
12 |
13 |
14 | class Legend :
15 |
16 | def GetResources ( self ):
17 | return {
18 | 'MenuText' : Title ,
19 | 'ToolTip' : Tooltip ,
20 | 'Pixmap' : 'Legend'
21 | }
22 |
23 | def Activated ( self ):
24 |
25 | plot = Plot.getPlot()
26 |
27 | if plot :
28 | isLegend = plot.isLegend()
29 | Plot.legend(not isLegend)
30 | return
31 |
32 | message = Qt.translate(
33 | 'plot_console' ,
34 | 'The legend must be activated on top of a plot document'
35 | )
36 |
37 | Console.PrintError(f'{ message }\n')
--------------------------------------------------------------------------------
/freecad/plot/Toolbar.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from FreeCAD.Plot import Plot # type: ignore
4 | from .PySide import QtWidgets , QtCore
5 | from FreeCAD import Gui , Qt
6 |
7 |
8 | translate = Qt.translate
9 |
10 | Toolbar_Title = translate('Plot','Plot')
11 |
12 | commands = [
13 | 'Plot_SaveFig' ,
14 | 'Plot_Axes' ,
15 | 'Plot_Series' ,
16 | 'Plot_Grid' ,
17 | 'Plot_Legend' ,
18 | 'Plot_Labels' ,
19 | 'Plot_Positions'
20 | ]
21 |
22 |
23 | def createToolbar ( workbench : Gui.Workbench ):
24 |
25 | workbench.appendToolbar(Toolbar_Title,commands)
26 |
27 | listenForActivation()
28 |
29 |
30 | def getToolbar ():
31 |
32 | window = Gui.getMainWindow()
33 |
34 | widgets = window.children()
35 |
36 | for widget in widgets:
37 |
38 | if not isinstance(widget,QtWidgets.QToolBar):
39 | continue
40 |
41 | if widget.objectName() != Toolbar_Title:
42 | continue
43 |
44 | return widget
45 |
46 | return None
47 |
48 |
49 | def listenForActivation ():
50 |
51 | def update ():
52 |
53 | plot = Plot.getPlot()
54 |
55 | active = bool( plot )
56 |
57 | toolbar = getToolbar()
58 |
59 | if toolbar :
60 | toolbar.setEnabled(active)
61 |
62 |
63 | Plot.getMdiArea().subWindowActivated.connect(update)
64 |
65 | QtCore.QTimer.singleShot(100,update)
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 |
93 | # Rope project settings
94 | .ropeproject
95 |
96 |
97 | freecad/plot/Resources/Interface/*.py
--------------------------------------------------------------------------------
/freecad/plot/Workbench.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 | from .Commands import Positions , Legend , Labels , Series , Axes , Grid , Save
4 | from .Toolbar import createToolbar
5 | from FreeCAD import Console , Gui , Qt
6 | from os.path import dirname , join
7 |
8 |
9 | __dir__ = dirname(__file__)
10 |
11 |
12 | translate = Qt.translate
13 |
14 | Workbench_Tooltip = translate('Workbench','The Plot module is used to edit/save output plots performed by other tools')
15 | Workbench_Title = translate('Workbench','Plot')
16 |
17 |
18 | class PlotWorkbench ( Gui.Workbench ):
19 |
20 | MenuText = Workbench_Title
21 | ToolTip = Workbench_Tooltip
22 |
23 | Icon = join(__dir__, 'Resources', 'Icons', 'Addon.svg')
24 |
25 |
26 | def __init__ ( self ):
27 |
28 | Gui.addLanguagePath(join(__dir__, 'Resources', 'Locales'))
29 | Gui.updateLocale()
30 |
31 | Gui.addIconPath(join(__dir__, 'Resources', 'Icons'))
32 |
33 | Gui.addCommand('Plot_SaveFig',Save())
34 | Gui.addCommand('Plot_Axes',Axes())
35 | Gui.addCommand('Plot_Series',Series())
36 | Gui.addCommand('Plot_Grid',Grid())
37 | Gui.addCommand('Plot_Legend',Legend())
38 | Gui.addCommand('Plot_Labels',Labels())
39 | Gui.addCommand('Plot_Positions',Positions())
40 |
41 |
42 | def Initialize ( self ):
43 |
44 | try:
45 | import matplotlib
46 | except ImportError:
47 | Console.PrintMessage(
48 | Qt.translate(
49 | 'plot_console', 'matplotlib not found, Plot module will be disabled'
50 | )
51 | + '\n'
52 | )
53 |
54 | createToolbar(self)
55 |
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Plot
2 |
3 |
4 | The Plot workbench provides some additional
5 | tools to modify Plots created within [FreeCAD].
6 |
7 | ![Preview]
8 |
9 |
10 |
11 | ## Tools
12 |
13 | ###
Save
14 |
15 | Extended plot saving dialog with more options.
16 |
17 | ###
Axes
18 |
19 | Modify axes ranges & scaling or add new ones.
20 |
21 | ###
Series
22 |
23 | Edit the style of series or remove them.
24 |
25 | ###
Grid
26 |
27 | Enable / disable the grid of the plot.
28 |
29 | ###
Legend
30 |
31 | Enable / disable the legend of the plot.
32 |
33 | ###
Labels
34 |
35 | Set the plot title and axes labels.
36 |
37 | ###
Positions & Sizes
38 |
39 | Resize and move plot elements
40 | like titles, labels and legends.
41 |
42 |
43 |
44 | ## Install
45 |
46 | This workbench is available for download via the FreeCAD [Addon Manager](https://wiki.freecadweb.org/Addon_manager)
47 |
48 | ## Usage
49 |
50 | Documentation for this workbench is available on the [Plot Workbench wiki page](https://wiki.freecadweb.org/Plot_Workbench)
51 |
52 | ## Tutorials
53 |
54 | * Official Plot Workbench Tutorial [Part 1](https://wiki.freecadweb.org/Plot_Basic_tutorial)
55 | * Official Plot Workbench Tutorial [Part 2](https://wiki.freecadweb.org/Plot_MultiAxes_tutorial)
56 |
57 |
58 |
59 | [Icon-Positions]: freecad/plot/Resources/Icons/Positions.svg
60 | [Icon-Labels]: freecad/plot/Resources/Icons/Labels.svg
61 | [Icon-Series]: freecad/plot/Resources/Icons/Series.svg
62 | [Icon-Legend]: freecad/plot/Resources/Icons/Legend.svg
63 | [Icon-Grid]: freecad/plot/Resources/Icons/Grid.svg
64 | [Icon-Axes]: freecad/plot/Resources/Icons/Axes.svg
65 | [Icon-Save]: freecad/plot/Resources/Icons/Save.svg
66 |
67 | [Preview]: Resources/Images/Preview-Toolbar.webp
68 |
69 | [FreeCAD]: https://freecad.org
--------------------------------------------------------------------------------
/freecad/plot/Backend.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from matplotlib.backends.backend_qt import NavigationToolbar2QT
5 | from matplotlib._pylab_helpers import Gcf
6 | from matplotlib.backend_bases import FigureManagerBase , FigureCanvasBase
7 | from matplotlib.pyplot import gca
8 | from .PySide import QtWidgets , QtCore
9 | from FreeCAD import Gui
10 |
11 |
12 | class PlotWidget ( QtWidgets.QWidget ):
13 |
14 | def __init__ ( self , manager , close_foo = None ):
15 |
16 | super(PlotWidget,self).__init__(manager.mdi)
17 |
18 | self.close_foo = close_foo
19 | self.manager = manager
20 |
21 |
22 | def closeEvent ( self , * args ):
23 |
24 | self.manager.close_foo()
25 |
26 | super(PlotWidget,self).closeEvent( * args )
27 |
28 |
29 | class FigureManager ( FigureManagerBase ):
30 |
31 | all_widgets = []
32 |
33 |
34 | def __init__ ( self , canvas , num ):
35 |
36 | super().__init__(canvas,num)
37 |
38 | self.mw = Gui.getMainWindow()
39 |
40 | self.mdi = self.mw.findChild(QtWidgets.QMdiArea)
41 |
42 | self.widget = PlotWidget(self)
43 | self.widget.setLayout(QtWidgets.QHBoxLayout())
44 |
45 | if self.mdi :
46 | self.mdi.addSubWindow(self.widget)
47 |
48 | layout = self.widget.layout()
49 |
50 | if not layout :
51 | return
52 |
53 | layout.addWidget(self.canvas) # type: ignore
54 |
55 | self.widget.show()
56 |
57 | FigureManager.all_widgets.append(self.widget)
58 |
59 | self.toolbar = NavigationToolbar2QT(self.canvas,self.widget,False)
60 | self.toolbar.setOrientation(QtCore.Qt.Orientation.Vertical)
61 |
62 | layout.addWidget(self.toolbar)
63 |
64 | self.canvas.set_widget_name = self.set_widget_name # type: ignore
65 |
66 |
67 | def show ( self ):
68 | self.canvas.draw_idle()
69 |
70 |
71 | def set_widget_name ( self ):
72 |
73 | if self.widget.windowTitle() :
74 | return
75 |
76 | title = gca().get_title()
77 |
78 | if title :
79 | self.widget.setWindowTitle(title)
80 |
81 |
82 | def close_foo ( self ):
83 |
84 | try:
85 | Gcf.destroy(self)
86 | except AttributeError:
87 | pass
88 |
89 |
90 | class FigureCanvas ( FigureCanvasBase ):
91 |
92 | def draw_idle ( self ):
93 |
94 | super().draw_idle()
95 |
96 | self.set_widget_name()
97 |
98 |
99 | def set_widget_name ( self ):
100 | pass
101 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
12 |
13 | 1.0.0
14 | 2025.10.29
15 | 2025-10-29
16 |
17 |
18 |
19 | matplotlib
20 |
21 |
22 |
23 | Tools to modify existing plots.
24 | Plot
25 |
26 |
27 |
28 | Legend
29 | Figure
30 | Plot
31 | Axes
32 | Grid
33 |
34 |
35 |
36 | PhoneDroid
39 |
40 |
41 |
42 | Jose Luis Cercós Pita
45 |
46 | looooo
47 | hasecilu
48 |
49 | PhoneDroid
52 |
53 |
54 |
55 | LGPL-2.1-or-later
58 |
59 | CC-BY-SA-4.0
62 |
63 |
64 |
65 | freecad/plot/Resources/Icons/Addon.svg
66 |
67 |
68 |
69 | https://wiki.freecad.org/Plot_Workbench
72 |
73 | https://github.com/FreeCAD/Plot
77 |
78 | https://github.com/FreeCAD/Plot/issues
81 |
82 | https://github.com/FreeCAD/Plot/blob/Latest/README.md
85 |
86 |
87 |
88 |
89 |
90 | ./
91 | PlotWorkbench
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Interface/Positions.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plot-Task-Positions
4 |
5 |
6 |
7 | 0
8 | 0
9 | 296
10 | 336
11 |
12 |
13 |
14 |
15 | 0
16 | 336
17 |
18 |
19 |
20 | Positions and Sizes
21 |
22 |
23 | -
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
31 | QAbstractItemView::NoEditTriggers
32 |
33 |
34 | true
35 |
36 |
37 |
38 | -
39 |
40 |
-
41 |
42 |
43 | Position
44 |
45 |
46 |
47 | -
48 |
49 |
50 | 3
51 |
52 |
53 | -99999.000000000000000
54 |
55 |
56 | 99999.000000000000000
57 |
58 |
59 | 0.010000000000000
60 |
61 |
62 |
63 | -
64 |
65 |
66 | 3
67 |
68 |
69 | -99999.000000000000000
70 |
71 |
72 | 99999.000000000000000
73 |
74 |
75 | 0.010000000000000
76 |
77 |
78 |
79 | -
80 |
81 |
82 | Size
83 |
84 |
85 |
86 | -
87 |
88 |
89 | 1
90 |
91 |
92 | 0.000000000000000
93 |
94 |
95 | 99999.000000000000000
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | ################################################################################
3 | # #
4 | # #
5 | # ###############################*********** #
6 | # ###############################************* #
7 | # ###############################************** #
8 | # ###############################**************** #
9 | # ###############################****************** #
10 | # ##########..............................********** #
11 | # ##########..............................********++ #
12 | # ##########..............................******++++ #
13 | # ##########..............................****++++++ #
14 | # ##########..............................***+++++++ #
15 | # ##########..............................*+++++++++ #
16 | # ##########..........++++++++++++++++++++++++++++++ #
17 | # ##########..........+++++++++++++++++++++++++++++ #
18 | # ##########..........++++++++++++++++++++++++++ #
19 | # ##########..........++++++++++++++++++++++++ #
20 | # ##########....................++++++++++++++ #
21 | # ##########....................++++++++++++++++ #
22 | # ##########....................++++++++++++++++++ #
23 | # ##########....................++++++++++++++++++++ #
24 | # ##########....................++++++++++++++++++++ #
25 | # ##########..........::::::::::+++++++++++++++++++ #
26 | # ##########..........++++++++++++++++++++++++++++ #
27 | # #########*..........++++++++++++++++++++++++++++ #
28 | # ########**..........+++++++++++++++++++ #
29 | # ######****..........++++++++++++++++++ #
30 | # ####******..........++++++++++++++++++ #
31 | # ##********..........+++++++++++++++++++ #
32 | # **********..........++++++++++++++++++++ #
33 | # ******************++++++++++++++++++++++ #
34 | # ****************++++++++ +++++++++ #
35 | # **************+++++++++ +++++ #
36 | # *************++++++++++ #
37 | # ***********+++++++++++ #
38 | # #
39 | # #
40 | ################################################################################
41 | # #
42 | # More details at #
43 | # https://EditorConfig.org #
44 | # #
45 | ################################################################################
46 |
47 |
48 | root = true
49 |
50 |
51 | [*.{toml,xml,py,md,svg}]
52 | insert_final_newline = false
53 | indent_style = space
54 | end_of_line = lf
55 | indent_size = 4
56 | charset = utf-8
57 |
58 | [*.{toml,xml,py,svg}]
59 | trim_trailing_whitespace = true
60 |
61 | [*.{md}]
62 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/freecad/plot/Resources/Interface/Labels.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plot-Task-Labels
4 |
5 |
6 |
7 | 0
8 | 0
9 | 276
10 | 228
11 |
12 |
13 |
14 |
15 | 0
16 | 0
17 |
18 |
19 |
20 | Set labels
21 |
22 |
23 | -
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
-
31 |
32 |
33 |
34 | 0
35 | 0
36 |
37 |
38 |
39 | Active axes:
40 |
41 |
42 |
43 | -
44 |
45 |
46 |
47 | 5
48 | 0
49 |
50 |
51 |
52 | 1
53 |
54 |
55 |
56 |
57 |
58 | -
59 |
60 |
61 | 0
62 |
63 |
64 | 6
65 |
66 |
-
67 |
68 |
69 | -
70 |
71 |
72 | Title
73 |
74 |
75 |
76 | -
77 |
78 |
79 | 1
80 |
81 |
82 | 1024
83 |
84 |
85 |
86 | -
87 |
88 |
89 | 1
90 |
91 |
92 | 1024
93 |
94 |
95 |
96 | -
97 |
98 |
99 | X label
100 |
101 |
102 |
103 | -
104 |
105 |
106 | -
107 |
108 |
109 | Y label
110 |
111 |
112 |
113 | -
114 |
115 |
116 | -
117 |
118 |
119 | 1
120 |
121 |
122 | 1024
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Interface/Save.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plot-Task-Save
4 |
5 |
6 |
7 | 0
8 | 0
9 | 260
10 | 253
11 |
12 |
13 |
14 | Save figure
15 |
16 |
17 | -
18 |
19 |
-
20 |
21 |
-
22 |
23 |
24 |
25 | 7
26 | 0
27 |
28 |
29 |
30 |
31 | -
32 |
33 |
34 | true
35 |
36 |
37 |
38 | 1
39 | 0
40 |
41 |
42 |
43 | ...
44 |
45 |
46 |
47 |
48 |
49 | -
50 |
51 |
-
52 |
53 |
54 | 0.010000000000000
55 |
56 |
57 | 99999.000000000000000
58 |
59 |
60 | 6.400000000000000
61 |
62 |
63 |
64 | -
65 |
66 |
67 |
68 | 0
69 | 0
70 |
71 |
72 |
73 | x
74 |
75 |
76 |
77 | -
78 |
79 |
80 | 0.010000000000000
81 |
82 |
83 | 99999.000000000000000
84 |
85 |
86 | 4.800000000000000
87 |
88 |
89 |
90 | -
91 |
92 |
93 |
94 | 0
95 | 0
96 |
97 |
98 |
99 | Inches
100 |
101 |
102 |
103 |
104 |
105 | -
106 |
107 |
-
108 |
109 |
110 | 1
111 |
112 |
113 | 2048
114 |
115 |
116 | 100
117 |
118 |
119 |
120 | -
121 |
122 |
123 |
124 | 0
125 | 0
126 |
127 |
128 |
129 | Dots per Inch
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/Save.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from FreeCAD.Plot import Plot # type: ignore
5 | from ..PySide import QtWidgets
6 | from os.path import splitext , dirname , extsep , join
7 | from FreeCAD import Console , Gui , Qt
8 | from os import getenv
9 | from re import search
10 |
11 |
12 | class TaskForm ( QtWidgets.QWidget ):
13 |
14 | pathButton : QtWidgets.QPushButton
15 | sizeLabel : QtWidgets.QLabel
16 | dpiLabel : QtWidgets.QLabel
17 | sizeX : QtWidgets.QDoubleSpinBox
18 | sizeY : QtWidgets.QDoubleSpinBox
19 | path : QtWidgets.QLineEdit
20 | dpi : QtWidgets.QSpinBox
21 |
22 |
23 | class TaskPanel:
24 |
25 | form : TaskForm
26 |
27 | name : str = 'plot save'
28 |
29 |
30 | def __init__ ( self ):
31 |
32 | path = join(
33 | dirname(__file__) , '..' ,
34 | 'Resources' , 'Interface' , 'Save.ui'
35 | )
36 |
37 | self.form = Gui.PySideUic.loadUi(path) # type: ignore
38 |
39 |
40 | def accept ( self ):
41 |
42 | plot = Plot.getPlot()
43 |
44 | if plot :
45 |
46 | form = self.form
47 |
48 | size = (
49 | form.sizeX.value() ,
50 | form.sizeY.value()
51 | )
52 |
53 | path = form.path.text()
54 | dpi = form.dpi.value()
55 |
56 | Plot.save(path,size,dpi)
57 |
58 | return True
59 |
60 | message = Qt.translate(
61 | 'plot_console' ,
62 | 'Plot document must be selected in order to save it'
63 | )
64 |
65 | Console.PrintError(f'{ message }\n')
66 |
67 | return False
68 |
69 |
70 | def isAllowedAlterSelection ( self ):
71 | return False
72 |
73 | def isAllowedAlterDocument ( self ):
74 | return False
75 |
76 | def isAllowedAlterView ( self ):
77 | return True
78 |
79 | def getStandardButtons ( self ):
80 | return QtWidgets.QDialogButtonBox.StandardButton.Save | QtWidgets.QDialogButtonBox.StandardButton.Cancel
81 |
82 | def needsFullSpace ( self ):
83 | return True
84 |
85 | def helpRequested ( self ):
86 | pass
87 |
88 | def clicked ( self , index ):
89 | pass
90 |
91 | def reject ( self ):
92 | return True
93 |
94 | def open ( self ):
95 | self.setupUi()
96 |
97 |
98 | def setupUi ( self ):
99 |
100 | home = getenv('USERPROFILE') or getenv('HOME')
101 |
102 | if not home:
103 | Console.PrintWarning('No home user / home directory found.')
104 | return
105 |
106 | path = join(home,'Plot.png')
107 |
108 | form = self.form
109 |
110 | form.path.setText(path)
111 |
112 | self.updateUI()
113 |
114 | form.pathButton.pressed.connect(self.onPathButton)
115 |
116 | Plot.getMdiArea().subWindowActivated.connect(self.onMdiArea)
117 |
118 |
119 | def updateUI ( self ):
120 |
121 | '''
122 | Setup UI controls values if possible
123 | '''
124 |
125 | plot = Plot.getPlot()
126 |
127 | enabled = bool(plot)
128 |
129 | form = self.form
130 |
131 | form.pathButton.setEnabled(enabled)
132 | form.sizeX.setEnabled(enabled)
133 | form.sizeY.setEnabled(enabled)
134 | form.path.setEnabled(enabled)
135 | form.dpi.setEnabled(enabled)
136 |
137 | if not plot:
138 | return
139 |
140 | figure = plot.fig
141 |
142 | size = figure.get_size_inches()
143 | dpi = figure.get_dpi()
144 |
145 | form.sizeX.setValue(size[ 0 ])
146 | form.sizeY.setValue(size[ 1 ])
147 | form.dpi.setValue(dpi)
148 |
149 |
150 | def onPathButton ( self ):
151 |
152 | '''
153 | Executed when the path selection button is pressed.
154 | '''
155 |
156 | form = self.form
157 |
158 | path = form.path.text()
159 |
160 | formats = [
161 | 'Portable Network Graphics (*.png)' ,
162 | 'Portable Document Format (*.pdf)' ,
163 | 'Encapsulated PostScript (*.eps)' ,
164 | 'PostScript (*.ps)'
165 | ]
166 |
167 | filters = str.join(';;',formats)
168 |
169 | [ path , format ] = QtWidgets.QFileDialog.getSaveFileName \
170 | (None,'Save figure',path,filters)
171 |
172 | print('Save Path',path)
173 |
174 | if path == '' :
175 | return
176 |
177 | [ root , extension ] = splitext(path)
178 |
179 | if extension == '' :
180 |
181 | match = search(r'(?<=\*\.)\w+',format)
182 |
183 | if match:
184 |
185 | extension = match.group(0)
186 |
187 | path = f'{ path }{ extsep }{ extension }'
188 |
189 | print('Path',path,extension)
190 |
191 | form.path.setText(path)
192 |
193 |
194 | def onMdiArea ( self , subWin ):
195 |
196 | '''
197 | Executed when a new window is selected on the mdi area.
198 |
199 | Keyword arguments:
200 | subWin -- Selected window.
201 | '''
202 |
203 | plot = Plot.getPlot()
204 |
205 | if plot != subWin :
206 | self.updateUI()
207 |
208 |
209 | def createTask ():
210 |
211 | panel = TaskPanel()
212 |
213 | Gui.Control.showDialog(panel)
214 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Interface/Series.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plot-Task-Series
4 |
5 |
6 |
7 | 0
8 | 0
9 | 296
10 | 336
11 |
12 |
13 |
14 |
15 | 0
16 | 336
17 |
18 |
19 |
20 | Configure series
21 |
22 |
23 | -
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
31 | QAbstractItemView::NoEditTriggers
32 |
33 |
34 | true
35 |
36 |
37 |
38 | -
39 |
40 |
-
41 |
42 |
43 |
44 | 2
45 | 0
46 |
47 |
48 |
49 |
50 | -
51 |
52 |
53 | 0.010000000000000
54 |
55 |
56 | 9999.000000000000000
57 |
58 |
59 | 0.500000000000000
60 |
61 |
62 | 1.000000000000000
63 |
64 |
65 |
66 | -
67 |
68 |
69 | Line style
70 |
71 |
72 |
73 | -
74 |
75 |
76 | Remove serie
77 |
78 |
79 |
80 | -
81 |
82 |
83 |
84 | 1
85 | 0
86 |
87 |
88 |
89 |
90 | -
91 |
92 |
93 | Markers
94 |
95 |
96 |
97 | -
98 |
99 |
100 |
101 | 1
102 | 0
103 |
104 |
105 |
106 | No label
107 |
108 |
109 |
110 | -
111 |
112 |
113 |
114 | 1
115 | 0
116 |
117 |
118 |
119 |
120 | -
121 |
122 |
123 |
124 | 1
125 | 0
126 |
127 |
128 |
129 | false
130 |
131 |
132 |
133 |
134 |
135 |
136 | -
137 |
138 |
139 | 1
140 |
141 |
142 | 9999
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/Labels.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from FreeCAD.Plot import Plot # type: ignore
5 | from ..PySide import QtWidgets
6 | from os.path import dirname , join
7 | from FreeCAD import Gui
8 |
9 |
10 | class TaskForm ( QtWidgets.QWidget ):
11 |
12 | axesIndex : QtWidgets.QSpinBox
13 |
14 | titleSize : QtWidgets.QSpinBox
15 | titleX : QtWidgets.QLineEdit
16 | titleY : QtWidgets.QLineEdit
17 | title : QtWidgets.QLineEdit
18 |
19 | xSize : QtWidgets.QSpinBox
20 | ySize : QtWidgets.QSpinBox
21 |
22 |
23 | class TaskPanel:
24 |
25 | form : TaskForm
26 |
27 | name = 'plot labels'
28 | skip = False
29 |
30 | def __init__ ( self ):
31 |
32 | path = join(
33 | dirname(__file__) , '..' ,
34 | 'Resources' , 'Interface' , 'Labels.ui'
35 | )
36 |
37 | self.form = Gui.PySideUic.loadUi(path) # type: ignore
38 |
39 |
40 |
41 | def isAllowedAlterSelection(self):
42 | return False
43 |
44 | def isAllowedAlterDocument(self):
45 | return False
46 |
47 | def isAllowedAlterView(self):
48 | return True
49 |
50 | def getStandardButtons ( self ):
51 | return QtWidgets.QDialogButtonBox.StandardButton.Close
52 |
53 | def needsFullSpace(self):
54 | return True
55 |
56 | def helpRequested(self):
57 | pass
58 |
59 | def clicked ( self , index ):
60 | pass
61 |
62 | def accept ( self ):
63 | return True
64 |
65 | def reject ( self ):
66 | return True
67 |
68 | def open ( self ):
69 | self.setupUi()
70 |
71 |
72 | def setupUi(self):
73 |
74 | # Look for active axes if can
75 |
76 | axesIndex = 0
77 |
78 | form = self.form
79 |
80 | plot = Plot.getPlot()
81 |
82 | if plot:
83 |
84 | while plot.axes != plot.axesList[axesIndex]:
85 | axesIndex = axesIndex + 1
86 |
87 | form.axesIndex.setValue(axesIndex)
88 |
89 | self.updateUI()
90 |
91 | form.titleSize.valueChanged.connect(self.onFontSizes)
92 | form.titleX.editingFinished.connect(self.onLabels)
93 | form.titleY.editingFinished.connect(self.onLabels)
94 | form.title.editingFinished.connect(self.onLabels)
95 |
96 | form.xSize.valueChanged.connect(self.onFontSizes)
97 | form.ySize.valueChanged.connect(self.onFontSizes)
98 |
99 | form.axesIndex.valueChanged.connect(self.onAxesId)
100 |
101 | Plot.getMdiArea().subWindowActivated.connect(self.onMdiArea)
102 |
103 |
104 | def onAxesId ( self , value ):
105 |
106 | '''
107 | Executed when axes index is modified.
108 | '''
109 |
110 | if self.skip :
111 | return
112 |
113 | self.skip = True
114 |
115 | # No active plot case
116 |
117 | plot = Plot.getPlot()
118 |
119 | if not plot:
120 | self.updateUI()
121 | self.skip = False
122 | return
123 |
124 | self.form.axesIndex.setMaximum(len(plot.axesList))
125 |
126 | if self.form.axesIndex.value() >= len(plot.axesList):
127 | self.form.axesIndex.setValue(len(plot.axesList) - 1)
128 |
129 | # Send new control to Plot instance
130 |
131 | plot.setActiveAxes(self.form.axesIndex.value())
132 |
133 | self.updateUI()
134 |
135 | self.skip = False
136 |
137 |
138 | def onLabels ( self ):
139 |
140 | '''
141 | Executed when labels have been modified.
142 | '''
143 |
144 | plot = Plot.getPlot()
145 |
146 | if not plot:
147 | self.updateUI()
148 | return
149 |
150 | Plot.title(str(self.form.title.text()))
151 |
152 | Plot.xlabel(str(self.form.titleX.text()))
153 | Plot.ylabel(str(self.form.titleY.text()))
154 |
155 | plot.update()
156 |
157 |
158 | def onFontSizes ( self , value ):
159 |
160 | '''
161 | Executed when font sizes have been modified.
162 | '''
163 |
164 | # Get apply environment
165 |
166 | plot = Plot.getPlot()
167 |
168 | if not plot:
169 | self.updateUI()
170 | return
171 |
172 | axes = plot.axes
173 |
174 | axes.title.set_fontsize(self.form.titleSize.value())
175 |
176 | axes.xaxis.label.set_fontsize(self.form.xSize.value())
177 | axes.yaxis.label.set_fontsize(self.form.ySize.value())
178 |
179 | plot.update()
180 |
181 |
182 | def onMdiArea ( self , subWin ):
183 |
184 | '''
185 | Executed when window is selected on mdi area.
186 |
187 | Keyword arguments:
188 | subWin -- Selected window.
189 | '''
190 |
191 | plt = Plot.getPlot()
192 |
193 | if plt != subWin:
194 | self.updateUI()
195 |
196 |
197 | def updateUI ( self ):
198 |
199 | '''
200 | Setup UI controls values if possible
201 | '''
202 |
203 | plot = Plot.getPlot()
204 |
205 | self.form.axesIndex.setEnabled(bool(plot))
206 | self.form.title.setEnabled(bool(plot))
207 | self.form.titleSize.setEnabled(bool(plot))
208 | self.form.titleX.setEnabled(bool(plot))
209 | self.form.xSize.setEnabled(bool(plot))
210 | self.form.titleY.setEnabled(bool(plot))
211 | self.form.ySize.setEnabled(bool(plot))
212 |
213 | if not plot:
214 | return
215 |
216 | # Ensure that active axes is correct
217 |
218 | index = min(self.form.axesIndex.value(), len(plot.axesList) - 1)
219 |
220 | self.form.axesIndex.setValue(index)
221 |
222 | # Store data before starting changing it.
223 |
224 | ax = plot.axes
225 |
226 | t = ax.get_title()
227 | x = ax.get_xlabel()
228 | y = ax.get_ylabel()
229 |
230 | tt = ax.title.get_fontsize()
231 | xx = ax.xaxis.label.get_fontsize()
232 | yy = ax.yaxis.label.get_fontsize()
233 |
234 | # Set labels
235 |
236 | self.form.title.setText(t)
237 | self.form.titleX.setText(x)
238 | self.form.titleY.setText(y)
239 |
240 | # Set font sizes
241 |
242 | self.form.titleSize.setValue(tt)
243 | self.form.xSize.setValue(xx)
244 | self.form.ySize.setValue(yy)
245 |
246 |
247 | def createTask ():
248 |
249 | panel = TaskPanel()
250 |
251 | Gui.Control.showDialog(panel)
252 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/Positions.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from FreeCAD.Plot import Plot # type: ignore
5 | from ..PySide import QtWidgets , QtCore
6 | from os.path import dirname , join
7 | from FreeCAD import Gui
8 |
9 |
10 | class TaskForm ( QtWidgets.QWidget ):
11 |
12 | sizeLabel : QtWidgets.QLabel
13 | posLabel : QtWidgets.QLabel
14 | items : QtWidgets.QListWidget
15 | Size : QtWidgets.QDoubleSpinBox
16 | X : QtWidgets.QDoubleSpinBox
17 | Y : QtWidgets.QDoubleSpinBox
18 |
19 |
20 | class TaskPanel :
21 |
22 | form : TaskForm
23 |
24 | objects = []
25 | names = []
26 | name = 'plot positions'
27 | skip = False
28 | item = 0
29 | plot = None
30 |
31 |
32 | def __init__ ( self ):
33 |
34 | path = join(
35 | dirname(__file__), '..' ,
36 | 'Resources' , 'Interface' , 'Positions.ui'
37 | )
38 |
39 | self.form = Gui.PySideUic.loadUi(path) # type: ignore
40 |
41 |
42 | def isAllowedAlterSelection ( self ):
43 | return False
44 |
45 | def isAllowedAlterDocument ( self ):
46 | return False
47 |
48 | def isAllowedAlterView ( self ):
49 | return True
50 |
51 | def getStandardButtons ( self ):
52 | return QtWidgets.QDialogButtonBox.StandardButton.Close
53 |
54 | def needsFullSpace ( self ):
55 | return True
56 |
57 | def helpRequested ( self ):
58 | pass
59 |
60 | def clicked ( self , index ):
61 | pass
62 |
63 | def accept ( self ):
64 | return True
65 |
66 | def reject ( self ):
67 | return True
68 |
69 | def open ( self ):
70 | self.setupUi()
71 |
72 |
73 | def setupUi ( self ):
74 |
75 | self.updateUI()
76 |
77 | form = self.form
78 |
79 | form.items.currentRowChanged.connect(self.onItem)
80 | form.Size.valueChanged.connect(self.onData)
81 | form.X.valueChanged.connect(self.onData)
82 | form.Y.valueChanged.connect(self.onData)
83 |
84 | Plot.getMdiArea().subWindowActivated.connect(self.onMdiArea)
85 |
86 |
87 | def onItem ( self , row ):
88 |
89 | '''
90 | Executed when selected item is modified.
91 | '''
92 |
93 | self.item = row
94 | self.updateUI()
95 |
96 |
97 | def onData ( self , value ):
98 |
99 | '''
100 | Executed when selected item data is modified.
101 | '''
102 |
103 | plot = Plot.getPlot()
104 |
105 | if not plot :
106 | self.updateUI()
107 | return
108 |
109 | if self.skip :
110 | return
111 |
112 | self.skip = True
113 |
114 | object = self.objects[ self.item ]
115 | name = self.names[ self.item ]
116 |
117 | form = self.form
118 |
119 | size = form.Size.value()
120 | x = form.X.value()
121 | y = form.Y.value()
122 |
123 | # x/y labels only have one position control
124 |
125 | if name.find('x label') >= 0 :
126 | form.Y.setValue(x)
127 | elif name.find('y label') >= 0 :
128 | form.X.setValue(y)
129 |
130 | # title and labels only have one size control
131 |
132 | if name.find('title') >= 0 or name.find('label') >= 0:
133 | object.set_position((x,y))
134 | object.set_size(size)
135 | else:
136 | # legend have all controls
137 | Plot.legend(plot.legend, (x, y), size)
138 |
139 | plot.update()
140 |
141 | self.skip = False
142 |
143 |
144 | def onMdiArea ( self , window : Plot ):
145 |
146 | plot = Plot.getPlot()
147 |
148 | if plot != window :
149 | self.updateUI()
150 |
151 |
152 | def updateUI ( self ):
153 |
154 | '''
155 | Setup the UI control values if it is possible.
156 | '''
157 |
158 | plot = Plot.getPlot()
159 |
160 | form = self.form
161 |
162 | enabled = bool(plot)
163 |
164 | form.items.setEnabled(enabled)
165 | form.Size.setEnabled(enabled)
166 | form.X.setEnabled(enabled)
167 | form.Y.setEnabled(enabled)
168 |
169 | if not plot :
170 | self.plot = plot
171 | form.items.clear()
172 | return
173 |
174 | # Refill items list only if Plot instance have been changed
175 |
176 | if self.plot != plot :
177 |
178 | self.plot = plot
179 |
180 | self.plot.update()
181 | self.setList()
182 |
183 | anyItems = len( self.objects ) > 0
184 |
185 | form.Size.setEnabled(anyItems)
186 | form.Y.setEnabled(anyItems)
187 | form.X.setEnabled(anyItems)
188 |
189 | if not anyItems :
190 | return
191 |
192 | # Get data for controls
193 |
194 | object = self.objects[ self.item ]
195 | name = self.names[ self.item ]
196 |
197 |
198 | if name.find('title') >= 0 or name.find('label') >= 0 :
199 |
200 | position = object.get_position()
201 |
202 | x = position[ 0 ]
203 | y = position[ 1 ]
204 |
205 | size = object.get_size()
206 |
207 | if name.find('x label') >= 0 :
208 | form.Y.setEnabled(False)
209 | form.Y.setValue(x)
210 | elif name.find('y label') >= 0 :
211 | form.X.setEnabled(False)
212 | form.X.setValue(y)
213 |
214 | else :
215 |
216 | x = plot.legPos[ 0 ]
217 | y = plot.legPos[ 1 ]
218 |
219 | texts = object.get_texts()
220 |
221 | if len( texts ) > 0 :
222 | size = texts[ -1 ].get_fontsize()
223 | else :
224 | size = 10
225 |
226 | # Send it to controls
227 |
228 | form.Size.setValue(size)
229 | form.X.setValue(x)
230 | form.Y.setValue(y)
231 |
232 |
233 | def setList ( self ):
234 |
235 | '''
236 | Setup UI controls values if possible
237 | '''
238 |
239 | # Clear lists
240 |
241 | self.objects = []
242 | self.names = []
243 |
244 | # Fill lists with available objects
245 |
246 | if self.plot:
247 |
248 | # Axes data
249 |
250 | for i in range(0, len(self.plot.axesList)):
251 |
252 | axes = self.plot.axesList[i]
253 |
254 | # Each axes have title, xaxis and yaxis
255 |
256 | title = axes.title
257 | text = title.get_text()
258 |
259 | if len( text ) > 0 :
260 | self.names.append(f'title (axes { i })')
261 | self.objects.append(title)
262 |
263 |
264 | label = axes.xaxis.label
265 | text = label.get_text()
266 |
267 | if len( text ) > 0 :
268 | self.objects.append(label)
269 | self.names.append(f'x label (axes { i })')
270 |
271 |
272 | label = axes.yaxis.label
273 | text = label.get_text()
274 |
275 | if len( text ) > 0 :
276 | self.objects.append(label)
277 | self.names.append(f'y label (axes { i })')
278 |
279 | # Legend if exist
280 |
281 | axes = self.plot.axesList[ -1 ]
282 |
283 | legend = axes.legend_
284 |
285 | if legend :
286 |
287 | if len( legend.get_texts() ) > 0 :
288 | self.objects.append(axes.legend_)
289 | self.names.append('legend')
290 |
291 |
292 | form = self.form
293 |
294 | # Send list to widget
295 |
296 | form.items.clear()
297 |
298 | for name in self.names:
299 | form.items.addItem(name)
300 |
301 | # Ensure that selected item is correct
302 |
303 | if self.item >= len(self.names):
304 |
305 | self.item = len(self.names) - 1
306 |
307 | index = form.items.indexAt(QtCore.QPoint(0,self.item))
308 |
309 | form.items.setCurrentIndex(index)
310 |
311 |
312 | def createTask ():
313 |
314 | panel = TaskPanel()
315 |
316 | Gui.Control.showDialog(panel)
317 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Interface/Axes.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plot-Task-Axes
4 |
5 |
6 |
7 | 0
8 | 0
9 | 276
10 | 416
11 |
12 |
13 |
14 |
15 | 0
16 | 416
17 |
18 |
19 |
20 | Configure axes
21 |
22 |
23 | -
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
-
31 |
32 |
33 |
34 | 0
35 | 0
36 |
37 |
38 |
39 | Active axes:
40 |
41 |
42 |
43 | -
44 |
45 |
46 |
47 | 5
48 | 0
49 |
50 |
51 |
52 | 1
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 | 2
61 | 0
62 |
63 |
64 |
65 | Add
66 |
67 |
68 |
69 | -
70 |
71 |
72 |
73 | 2
74 | 0
75 |
76 |
77 |
78 |
79 | 10
80 | 0
81 |
82 |
83 |
84 | Remove
85 |
86 |
87 |
88 |
89 |
90 | -
91 |
92 |
93 | Apply to all axes
94 |
95 |
96 |
97 | -
98 |
99 |
100 | 0
101 |
102 |
103 | 6
104 |
105 |
-
106 |
107 |
108 |
109 | 0
110 | 1
111 |
112 |
113 |
114 | 100
115 |
116 |
117 | 90
118 |
119 |
120 | Qt::Vertical
121 |
122 |
123 |
124 | -
125 |
126 |
127 | 100
128 |
129 |
130 | 90
131 |
132 |
133 | Qt::Horizontal
134 |
135 |
136 |
137 | -
138 |
139 |
140 | 100
141 |
142 |
143 | 10
144 |
145 |
146 | Qt::Horizontal
147 |
148 |
149 |
150 | -
151 |
152 |
153 |
154 | 0
155 | 1
156 |
157 |
158 |
159 | 100
160 |
161 |
162 | 10
163 |
164 |
165 | Qt::Vertical
166 |
167 |
168 |
169 | -
170 |
171 |
172 | Dimensions:
173 |
174 |
175 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
176 |
177 |
178 |
179 | -
180 |
181 |
182 | Qt::Vertical
183 |
184 |
185 |
186 | 20
187 | 40
188 |
189 |
190 |
191 |
192 | -
193 |
194 |
-
195 |
196 |
197 | Y Position
198 |
199 |
200 | Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft
201 |
202 |
203 |
204 | -
205 |
206 |
207 | 0
208 |
209 |
-
210 |
211 | Left
212 |
213 |
214 | -
215 |
216 | Right
217 |
218 |
219 |
220 |
221 | -
222 |
223 |
224 | 99999
225 |
226 |
227 |
228 |
229 |
230 | -
231 |
232 |
-
233 |
234 |
235 | X Position
236 |
237 |
238 | Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft
239 |
240 |
241 |
242 | -
243 |
244 |
245 | 0
246 |
247 |
-
248 |
249 | Bottom
250 |
251 |
252 | -
253 |
254 | Top
255 |
256 |
257 |
258 |
259 | -
260 |
261 |
262 | 99999
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 | -
271 |
272 |
-
273 |
274 |
275 | Scales
276 |
277 |
278 |
279 | -
280 |
281 |
282 | Automatic
283 |
284 |
285 |
286 | -
287 |
288 |
289 | Automatic
290 |
291 |
292 |
293 | -
294 |
295 |
296 |
297 |
298 |
299 |
300 | -
301 |
302 |
303 |
304 |
305 |
306 |
307 | -
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | -
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/Series.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from matplotlib.colors import colorConverter
5 | from matplotlib.lines import Line2D
6 | from FreeCAD.Plot import Plot # type: ignore
7 | from ..PySide import QtWidgets , QtCore
8 | from os.path import dirname , join
9 | from FreeCAD import Gui
10 |
11 |
12 |
13 | class TaskForm ( QtWidgets.QWidget ):
14 |
15 | markerLabel : QtWidgets.QLabel
16 | markerSize : QtWidgets.QSpinBox
17 | styleLabel : QtWidgets.QLabel
18 | lineWidth : QtWidgets.QDoubleSpinBox
19 | lineStyle : QtWidgets.QComboBox
20 | markers : QtWidgets.QComboBox
21 | isLabel : QtWidgets.QCheckBox
22 | remove : QtWidgets.QPushButton
23 | items : QtWidgets.QListWidget
24 | color : QtWidgets.QPushButton
25 | label : QtWidgets.QLineEdit
26 |
27 |
28 | class TaskPanel :
29 |
30 | form : TaskForm
31 |
32 | plot : object = None
33 | skip : bool = False
34 | name : str = 'plot series editor'
35 | item : int = 0
36 |
37 |
38 | def __init__ ( self ):
39 |
40 | path = join(
41 | dirname(__file__) , '..' ,
42 | 'Resources' , 'Interface' , 'Series.ui'
43 | )
44 |
45 | self.form = Gui.PySideUic.loadUi(path) # type: ignore
46 |
47 |
48 | def isAllowedAlterSelection ( self ):
49 | return False
50 |
51 | def isAllowedAlterDocument ( self ):
52 | return False
53 |
54 | def isAllowedAlterView ( self ):
55 | return True
56 |
57 | def getStandardButtons ( self ):
58 | return QtWidgets.QDialogButtonBox.StandardButton.Close
59 |
60 | def needsFullSpace ( self ):
61 | return True
62 |
63 | def helpRequested ( self ):
64 | pass
65 |
66 | def clicked ( self , index ):
67 | pass
68 |
69 | def accept ( self ):
70 | return True
71 |
72 | def reject ( self ):
73 | return True
74 |
75 | def open ( self ):
76 | self.setupUi()
77 |
78 |
79 | def setupUi ( self ):
80 |
81 | self.fillStyles()
82 | self.updateUI()
83 |
84 | form = self.form
85 |
86 | form.markerSize.valueChanged.connect(self.onData)
87 | form.lineStyle.currentIndexChanged.connect(self.onData)
88 | form.lineWidth.valueChanged.connect(self.onData)
89 | form.markers.currentIndexChanged.connect(self.onData)
90 | form.isLabel.stateChanged.connect(self.onData)
91 | form.label.editingFinished.connect(self.onData)
92 |
93 | form.remove.pressed.connect(self.onRemove)
94 | form.items.currentRowChanged.connect(self.onItem)
95 | form.color.pressed.connect(self.onColor)
96 |
97 | Plot.getMdiArea().subWindowActivated.connect(self.onMdiArea)
98 |
99 |
100 | def fillStyles ( self ):
101 |
102 | '''
103 | Fill the style combo boxes with the available ones.
104 | '''
105 |
106 | form = self.form
107 |
108 | # Line styles
109 |
110 | for style in Line2D.lineStyles.keys():
111 |
112 | string = '\'' + str(style) + '\''
113 | string += ' (' + Line2D.lineStyles[style] + ')'
114 |
115 | form.lineStyle.addItem(string)
116 |
117 | # Markers
118 |
119 | for marker in Line2D.markers.keys():
120 |
121 | string = '\'' + str(marker) + '\''
122 | string += ' (' + Line2D.markers[marker] + ')'
123 |
124 | form.markers.addItem(string)
125 |
126 |
127 | def onItem ( self , row ):
128 |
129 | '''
130 | Executed when the selected item is modified.
131 | '''
132 |
133 | if self.skip:
134 | return
135 |
136 | self.skip = True
137 |
138 | self.item = row
139 |
140 | self.updateUI()
141 |
142 | self.skip = False
143 |
144 |
145 | def onData ( self ):
146 |
147 | '''
148 | Executed when the selected item data is modified.
149 | '''
150 |
151 | if self.skip:
152 | return
153 |
154 | self.skip = True
155 |
156 | plot = Plot.getPlot()
157 |
158 | if not plot:
159 | self.updateUI()
160 | return
161 |
162 | # Ensure that selected series exist
163 |
164 | if self.item >= len(Plot.series()):
165 | self.updateUI()
166 | return
167 |
168 | # Set label
169 |
170 | serie = Plot.series()[ self.item ]
171 |
172 | if(self.form.isLabel.isChecked()):
173 | serie.name = None
174 | self.form.label.setEnabled(False)
175 | else:
176 | serie.name = self.form.label.text()
177 | self.form.label.setEnabled(True)
178 |
179 | # Set line style and marker
180 |
181 | style = self.form.lineStyle.currentIndex()
182 | linestyles = list(Line2D.lineStyles.keys())
183 | serie.line.set_linestyle(linestyles[style])
184 | marker = self.form.markers.currentIndex()
185 | markers = list(Line2D.markers.keys())
186 | serie.line.set_marker(markers[marker])
187 |
188 | # Set line width and marker size
189 |
190 | serie.line.set_linewidth(self.form.lineWidth.value())
191 | serie.line.set_markersize(self.form.markerSize.value())
192 |
193 | plot.update()
194 |
195 | # Regenerate series labels
196 |
197 | self.setList()
198 | self.skip = False
199 |
200 |
201 | def onColor ( self ):
202 |
203 | '''
204 | Executed when color palette is requested.
205 | '''
206 |
207 | plot = Plot.getPlot()
208 |
209 | if not plot:
210 | self.updateUI()
211 | return
212 |
213 | # Ensure that selected serie exist
214 |
215 | if self.item >= len(Plot.series()):
216 | self.updateUI()
217 | return
218 |
219 | # Show widget to select color
220 |
221 | col = QtWidgets.QColorDialog.getColor()
222 |
223 | # Send color to widget and serie
224 |
225 | if col.isValid():
226 |
227 | serie = plot.series[self.item]
228 |
229 | self.form.color.setStyleSheet(
230 | f'background-color: rgb({ col.red() }, { col.green() }, { col.blue() });'
231 | )
232 |
233 | serie.line.set_color((
234 | col.redF() ,
235 | col.greenF() ,
236 | col.blueF()
237 | ))
238 |
239 | plot.update()
240 |
241 |
242 | def onRemove ( self ):
243 |
244 | '''
245 | Executed when the data serie must be removed.
246 | '''
247 |
248 | plt = Plot.getPlot()
249 |
250 | if not plt:
251 | self.updateUI()
252 | return
253 |
254 | # Ensure that selected serie exist
255 |
256 | if self.item >= len(Plot.series()):
257 | self.updateUI()
258 | return
259 |
260 | # Remove serie
261 |
262 | removeSeries(self.item)
263 |
264 | self.setList()
265 | self.updateUI()
266 |
267 | plt.update()
268 |
269 |
270 | def onMdiArea(self, subWin):
271 |
272 | '''
273 | Executed when a new window is selected on the mdi area.
274 |
275 | Keyword arguments:
276 | subWin -- Selected window.
277 | '''
278 |
279 | plt = Plot.getPlot()
280 |
281 | if plt != subWin:
282 | self.updateUI()
283 |
284 |
285 | def updateUI ( self ):
286 |
287 | '''
288 | Setup UI controls values if possible
289 | '''
290 |
291 | form = self.form
292 | plot = Plot.getPlot()
293 |
294 | enabled = bool(plot)
295 |
296 | form.markerSize.setEnabled(enabled)
297 | form.lineStyle.setEnabled(enabled)
298 | form.lineWidth.setEnabled(enabled)
299 | form.markers.setEnabled(enabled)
300 | form.isLabel.setEnabled(enabled)
301 | form.remove.setEnabled(enabled)
302 | form.items.setEnabled(enabled)
303 | form.label.setEnabled(enabled)
304 | form.color.setEnabled(enabled)
305 |
306 | if not plot:
307 | self.plot = None
308 | form.items.clear()
309 | return
310 |
311 | self.skip = True
312 |
313 | # Refill list
314 |
315 | series = Plot.series()
316 |
317 | if self.plot != plot or len(series) != form.items.count():
318 | self.plot = plot
319 | self.setList()
320 |
321 | # Ensure that have series
322 |
323 | if not len(series):
324 | form.markerSize.setEnabled(False)
325 | form.lineStyle.setEnabled(False)
326 | form.lineWidth.setEnabled(False)
327 | form.isLabel.setEnabled(False)
328 | form.markers.setEnabled(False)
329 | form.remove.setEnabled(False)
330 | form.label.setEnabled(False)
331 | form.color.setEnabled(False)
332 | return
333 |
334 | # Set label
335 |
336 | serie = series[ self.item ]
337 |
338 | if serie.name is None:
339 | form.isLabel.setChecked(True)
340 | form.label.setEnabled(False)
341 | form.label.setText('')
342 | else:
343 | form.isLabel.setChecked(False)
344 | form.label.setText(serie.name)
345 |
346 | # Set line style and marker
347 |
348 | form.lineStyle.setCurrentIndex(0)
349 |
350 | for i, style in enumerate(Line2D.lineStyles.keys()):
351 | if style == serie.line.get_linestyle():
352 | form.lineStyle.setCurrentIndex(i)
353 |
354 | form.markers.setCurrentIndex(0)
355 |
356 | for i, marker in enumerate(Line2D.markers.keys()):
357 | if marker == serie.line.get_marker():
358 | form.markers.setCurrentIndex(i)
359 |
360 | # Set line width and marker size
361 |
362 | form.markerSize.setValue(serie.line.get_markersize())
363 | form.lineWidth.setValue(serie.line.get_linewidth())
364 |
365 | # Set color
366 |
367 | color = colorConverter.to_rgb(serie.line.get_color())
368 |
369 | green = int(color[1] * 255)
370 | blue = int(color[2] * 255)
371 | red = int(color[0] * 255)
372 |
373 | form.color.setStyleSheet(
374 | f'background-color: rgb({ red },{ green },{ blue });'
375 | )
376 |
377 | self.skip = False
378 |
379 |
380 | def setList(self):
381 |
382 | '''
383 | Setup the UI control values if it is possible.
384 | '''
385 |
386 | form = self.form
387 |
388 | form.items.clear()
389 |
390 | series = Plot.series()
391 |
392 | for i in range(0, len(series)):
393 |
394 | serie = series[i]
395 | string = 'serie ' + str(i) + ': '
396 |
397 | if serie.name is None:
398 | string = string + '\'No label\''
399 | else:
400 | string = string + serie.name
401 |
402 | form.items.addItem(string)
403 |
404 | # Ensure that selected item is correct
405 |
406 | if len(series) and self.item >= len(series):
407 |
408 | self.item = len(series) - 1
409 |
410 | index = form.items.indexAt(QtCore.QPoint(0,self.item))
411 |
412 | form.items.setCurrentIndex(index)
413 |
414 |
415 | def createTask ():
416 |
417 | panel = TaskPanel()
418 |
419 | Gui.Control.showDialog(panel)
420 |
421 |
422 | def removeSeries ( index : int ):
423 |
424 | plot = Plot.getPlot()
425 |
426 | if not plot :
427 | return
428 |
429 | series = plot.series
430 |
431 | if not series :
432 | return
433 |
434 | serie = series[ index ]
435 |
436 | if not serie :
437 | return
438 |
439 | axes = serie.axes
440 |
441 | axes.lines[ serie.lid ].remove()
442 |
443 | del plot.series[ index ]
444 |
445 | plot.update()
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Series.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
150 |
--------------------------------------------------------------------------------
/Resources/Locales/ja.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Plot
6 |
7 |
8 | Plot edition tools
9 |
10 |
11 |
12 |
13 | Plot
14 | PPPPPLLLLLOOOOTTTT
15 |
16 |
17 |
18 | Plot-Task-Axes
19 |
20 |
21 | Configure axes
22 |
23 |
24 |
25 |
26 | Active axes:
27 |
28 |
29 |
30 |
31 | add
32 |
33 |
34 |
35 |
36 | del
37 |
38 |
39 |
40 |
41 | Apply to all axes
42 |
43 |
44 |
45 |
46 | Dimensions:
47 |
48 |
49 |
50 |
51 | Y axis position
52 |
53 |
54 |
55 |
56 | Left
57 |
58 |
59 |
60 |
61 | Right
62 |
63 |
64 |
65 |
66 | Bottom
67 |
68 |
69 |
70 |
71 | Top
72 |
73 |
74 |
75 |
76 | X axis position
77 |
78 |
79 |
80 |
81 | Scales
82 |
83 |
84 |
85 |
86 | X auto
87 |
88 |
89 |
90 |
91 | Y auto
92 |
93 |
94 |
95 |
96 | Plot-Task-Labels
97 |
98 |
99 | Set labels
100 |
101 |
102 |
103 |
104 | Active axes:
105 |
106 |
107 |
108 |
109 | Title
110 |
111 |
112 |
113 |
114 | X label
115 |
116 |
117 |
118 |
119 | Y label
120 |
121 |
122 |
123 |
124 | Plot-Task-Positions
125 |
126 |
127 | Set positions and sizes
128 |
129 |
130 |
131 |
132 | Position
133 |
134 |
135 |
136 |
137 | Size
138 |
139 |
140 |
141 |
142 | Plot-Task-Save
143 |
144 |
145 | Save figure
146 |
147 |
148 |
149 |
150 | ...
151 |
152 |
153 |
154 |
155 | x
156 |
157 |
158 |
159 |
160 | Inches
161 |
162 |
163 |
164 |
165 | Dots per Inch
166 |
167 |
168 |
169 |
170 | Plot-Task-Series
171 |
172 |
173 | Configure series
174 |
175 |
176 |
177 |
178 | Line style
179 |
180 |
181 |
182 |
183 | Remove serie
184 |
185 |
186 |
187 |
188 | Markers
189 |
190 |
191 |
192 |
193 | No label
194 |
195 |
196 |
197 |
198 | Plot_Axes
199 |
200 |
201 | Configure the axes parameters
202 |
203 |
204 |
205 |
206 | Configure axes
207 |
208 |
209 |
210 |
211 | Plot_Grid
212 |
213 |
214 | Show/Hide grid on selected plot
215 |
216 |
217 |
218 |
219 | Show/Hide grid
220 |
221 |
222 |
223 |
224 | Plot_Labels
225 |
226 |
227 | Set title and axes labels
228 |
229 |
230 |
231 |
232 | Set labels
233 |
234 |
235 |
236 |
237 | Plot_Legend
238 |
239 |
240 | Show/Hide legend on selected plot
241 |
242 |
243 |
244 |
245 | Show/Hide legend
246 |
247 |
248 |
249 |
250 | Plot_Positions
251 |
252 |
253 | Set labels and legend positions and sizes
254 |
255 |
256 |
257 |
258 | Set positions and sizes
259 |
260 |
261 |
262 |
263 | Plot_SaveFig
264 |
265 |
266 | Save the plot as an image file
267 |
268 |
269 |
270 |
271 | Save plot
272 |
273 |
274 |
275 |
276 | Plot_Series
277 |
278 |
279 | Configure series drawing style and label
280 |
281 |
282 |
283 |
284 | Configure series
285 |
286 |
287 |
288 |
289 | Workbench
290 |
291 |
292 | The Plot module is used to edit/save output plots performed by other tools
293 |
294 |
295 |
296 |
297 | Plot
298 | PPPPPLLLLLOOOOTTTT
299 |
300 |
301 |
302 | plot_console
303 |
304 |
305 | Plot document must be selected in order to save it
306 |
307 |
308 |
309 |
310 | Axes 0 can not be deleted
311 |
312 |
313 |
314 |
315 | matplotlib style sheets not found
316 |
317 |
318 |
319 |
320 | matplotlib not found, Plot module will be disabled
321 |
322 |
323 |
324 |
325 | The legend must be activated on top of a plot document
326 |
327 |
328 |
329 |
330 | The grid must be activated on top of a plot document
331 |
332 |
333 |
334 |
335 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Axes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
184 |
--------------------------------------------------------------------------------
/freecad/plot/Panels/Axes.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: LGPL-2.1-or-later
2 |
3 |
4 | from matplotlib.spines import Spines
5 | from FreeCAD.Plot import Plot # type: ignore
6 | from ..PySide import QtWidgets
7 | from os.path import dirname , join
8 | from FreeCAD import Console , Gui , Qt
9 |
10 |
11 | class TaskForm ( QtWidgets.QWidget ):
12 |
13 | newAxesButton : QtWidgets.QPushButton
14 | delAxesButton : QtWidgets.QPushButton
15 | axesIndex : QtWidgets.QSpinBox
16 | allAxes : QtWidgets.QCheckBox
17 | xOffset : QtWidgets.QSpinBox
18 | yOffset : QtWidgets.QSpinBox
19 | posXMin : QtWidgets.QSlider
20 | posXMax : QtWidgets.QSlider
21 | posYMin : QtWidgets.QSlider
22 | posYMax : QtWidgets.QSlider
23 | xAlign : QtWidgets.QComboBox
24 | yAlign : QtWidgets.QComboBox
25 | xAuto : QtWidgets.QCheckBox
26 | yAuto : QtWidgets.QCheckBox
27 | xMin : QtWidgets.QLineEdit
28 | yMin : QtWidgets.QLineEdit
29 | xMax : QtWidgets.QLineEdit
30 | yMax : QtWidgets.QLineEdit
31 |
32 |
33 | class TaskPanel:
34 |
35 | form : TaskForm
36 |
37 | name = 'plot axes'
38 | skip = False
39 |
40 |
41 | def __init__ ( self ):
42 |
43 | path = join(
44 | dirname(__file__),'..' ,
45 | 'Resources' , 'Interface' , 'Axes.ui'
46 | )
47 |
48 | self.form = Gui.PySideUic.loadUi(path) # type: ignore
49 |
50 |
51 | def isAllowedAlterSelection ( self ):
52 | return False
53 |
54 | def isAllowedAlterDocument ( self ):
55 | return False
56 |
57 | def isAllowedAlterView ( self ):
58 | return True
59 |
60 | def getStandardButtons ( self ):
61 | return QtWidgets.QDialogButtonBox.StandardButton.Close
62 |
63 | def needsFullSpace ( self ):
64 | return True
65 |
66 | def helpRequested ( self ):
67 | pass
68 |
69 | def accept ( self ):
70 | return True
71 |
72 | def reject ( self ):
73 | return True
74 |
75 | def clicked ( self , index ):
76 | pass
77 |
78 | def open ( self ):
79 | self.setupUi()
80 |
81 |
82 | def setupUi ( self ):
83 |
84 | # Look for active axes if can
85 |
86 | axId = 0
87 |
88 | form = self.form
89 |
90 | plot = Plot.getPlot()
91 |
92 | if plot:
93 |
94 | while plot.axes != plot.axesList[axId]:
95 | axId = axId + 1
96 |
97 | form.axesIndex.setValue(axId)
98 |
99 | self.updateUI()
100 |
101 | form.axesIndex.valueChanged.connect(self.onAxesId)
102 |
103 | form.delAxesButton.pressed.connect(self.onRemove)
104 | form.newAxesButton.pressed.connect(self.onNew)
105 |
106 | form.posXMin.valueChanged.connect(self.onDims)
107 | form.posXMax.valueChanged.connect(self.onDims)
108 | form.posYMin.valueChanged.connect(self.onDims)
109 | form.posYMax.valueChanged.connect(self.onDims)
110 |
111 | form.xAlign.currentIndexChanged.connect(self.onAlign)
112 | form.yAlign.currentIndexChanged.connect(self.onAlign)
113 |
114 | form.xOffset.valueChanged.connect(self.onOffset)
115 | form.yOffset.valueChanged.connect(self.onOffset)
116 |
117 | form.xAuto.stateChanged.connect(self.onScales)
118 | form.yAuto.stateChanged.connect(self.onScales)
119 | form.xMin.editingFinished.connect(self.onScales)
120 | form.xMax.editingFinished.connect(self.onScales)
121 | form.yMin.editingFinished.connect(self.onScales)
122 | form.yMax.editingFinished.connect(self.onScales)
123 |
124 | Plot.getMdiArea().subWindowActivated.connect(self.onMdiArea)
125 |
126 |
127 | def onAxesId ( self , value ):
128 |
129 | '''
130 | Executed when axes index is modified.
131 | '''
132 |
133 | if self.skip :
134 | return
135 |
136 | self.skip = True
137 |
138 | # No active plot case
139 |
140 | plot = Plot.getPlot()
141 |
142 | if not plot:
143 | self.updateUI()
144 | self.skip = False
145 | return
146 |
147 | form = self.form
148 |
149 | form.axesIndex.setMaximum(len(plot.axesList))
150 |
151 | if form.axesIndex.value() >= len(plot.axesList):
152 | form.axesIndex.setValue(len(plot.axesList) - 1)
153 |
154 | # Send new control to Plot instance
155 |
156 | plot.setActiveAxes(form.axesIndex.value())
157 |
158 | self.updateUI()
159 |
160 | self.skip = False
161 |
162 |
163 | def onNew ( self ):
164 |
165 | '''
166 | Executed when new axes must be created.
167 | '''
168 |
169 | # Ensure that we can work
170 |
171 | plot = Plot.getPlot()
172 |
173 | if not plot:
174 | self.updateUI()
175 | return
176 |
177 | Plot.addNewAxes()
178 |
179 | form = self.form
180 |
181 | form.axesIndex.setValue(len(plot.axesList) - 1)
182 |
183 | plot.update()
184 |
185 |
186 | def onRemove ( self ):
187 |
188 | '''
189 | Executed when axes must be deleted.
190 | '''
191 |
192 | # Ensure that we can work
193 |
194 | plot = Plot.getPlot()
195 |
196 | if not plot:
197 | self.updateUI()
198 | return
199 |
200 | form = self.form
201 |
202 | # Don't remove first axes
203 |
204 | if not form.axesIndex.value():
205 |
206 | message = Qt.translate(
207 | 'plot_console',
208 | 'Axes 0 can not be deleted'
209 | )
210 |
211 | Console.PrintError(f'{ message }\n')
212 |
213 | return
214 |
215 | # Remove axes
216 |
217 | ax = plot.axes
218 | ax.set_axis_off()
219 |
220 | plot.axesList.pop(form.axesIndex.value())
221 |
222 | # Ensure that active axes is correct
223 |
224 | index = min(form.axesIndex.value(), len(plot.axesList) - 1)
225 |
226 | form.axesIndex.setValue(index)
227 |
228 | plot.update()
229 |
230 |
231 | def onDims ( self , value ):
232 |
233 | '''
234 | Executed when axes dims have been modified.
235 | '''
236 |
237 | # Ensure that we can work
238 |
239 | plot = Plot.getPlot()
240 |
241 | if not plot:
242 | self.updateUI()
243 | return
244 |
245 | axesList = [plot.axes]
246 |
247 | if self.form.allAxes.isChecked():
248 | axesList = plot.axesList
249 |
250 | # Set new dimensions
251 |
252 | xmin = self.form.posXMin.value() / 100.0
253 | xmax = self.form.posXMax.value() / 100.0
254 |
255 | ymin = self.form.posYMin.value() / 100.0
256 | ymax = self.form.posYMax.value() / 100.0
257 |
258 | for axes in axesList:
259 | axes.set_position([xmin, ymin, xmax - xmin, ymax - ymin])
260 |
261 | plot.update()
262 |
263 |
264 | def onAlign ( self , value ):
265 |
266 | '''
267 | Executed when axes align have been modified.
268 | '''
269 |
270 | # Ensure that we can work
271 |
272 | plot = Plot.getPlot()
273 |
274 | if not plot:
275 | self.updateUI()
276 | return
277 |
278 | axesList = [plot.axes]
279 |
280 | if self.form.allAxes.isChecked():
281 | axesList = plot.axesList
282 |
283 | # Set new alignment
284 |
285 | for axes in axesList:
286 |
287 | if self.form.xAlign.currentIndex() == 0:
288 | axes.xaxis.tick_bottom()
289 | axes.spines['bottom'].set_color((0.0, 0.0, 0.0))
290 | axes.spines['top'].set_color('none')
291 | axes.xaxis.set_ticks_position('bottom')
292 | axes.xaxis.set_label_position('bottom')
293 | else:
294 | axes.xaxis.tick_top()
295 | axes.spines['top'].set_color((0.0, 0.0, 0.0))
296 | axes.spines['bottom'].set_color('none')
297 | axes.xaxis.set_ticks_position('top')
298 | axes.xaxis.set_label_position('top')
299 |
300 | if self.form.yAlign.currentIndex() == 0:
301 | axes.yaxis.tick_left()
302 | axes.spines['left'].set_color((0.0, 0.0, 0.0))
303 | axes.spines['right'].set_color('none')
304 | axes.yaxis.set_ticks_position('left')
305 | axes.yaxis.set_label_position('left')
306 | else:
307 | axes.yaxis.tick_right()
308 | axes.spines['right'].set_color((0.0, 0.0, 0.0))
309 | axes.spines['left'].set_color('none')
310 | axes.yaxis.set_ticks_position('right')
311 | axes.yaxis.set_label_position('right')
312 |
313 | plot.update()
314 |
315 |
316 | def onOffset ( self , value ):
317 |
318 | '''
319 | Executed when axes offsets have been modified.
320 | '''
321 |
322 | # Ensure that we can work
323 |
324 | plot = Plot.getPlot()
325 |
326 | if not plot:
327 | self.updateUI()
328 | return
329 |
330 | form = self.form
331 |
332 | axesList = [ plot.axes ]
333 |
334 | if form.allAxes.isChecked():
335 | axesList = plot.axesList
336 |
337 | # Set new offset
338 |
339 | for axes in axesList:
340 |
341 | # For some reason, modify spines offset erase axes labels, so we
342 | # need store it in order to regenerate later
343 |
344 | x = axes.get_xlabel()
345 | y = axes.get_ylabel()
346 |
347 | spines : Spines = axes.spines
348 |
349 | for loc , spine in spines.items() :
350 |
351 | if loc in [ 'bottom', 'top' ]:
352 | spine.set_position(('outward',form.xOffset.value()))
353 |
354 | if loc in [ 'left' , 'right' ]:
355 | spine.set_position(('outward',form.yOffset.value()))
356 |
357 | # Now we can restore axes labels
358 |
359 | Plot.xlabel(str(x))
360 | Plot.ylabel(str(y))
361 |
362 | plot.update()
363 |
364 |
365 | def onScales ( self ):
366 |
367 | '''
368 | Executed when axes scales have been modified.
369 | '''
370 |
371 | # Ensure that we can work
372 |
373 | plot = Plot.getPlot()
374 |
375 | if not plot:
376 | self.updateUI()
377 | return
378 |
379 | axesList = [plot.axes]
380 |
381 | if self.form.allAxes.isChecked():
382 | axesList = plot.axesList
383 |
384 | if self.skip :
385 | return
386 |
387 | self.skip = True
388 |
389 | # X axis
390 |
391 | if self.form.xAuto.isChecked():
392 |
393 | for ax in axesList:
394 | ax.set_autoscalex_on(True)
395 |
396 | self.form.xMin.setEnabled(False)
397 | self.form.xMax.setEnabled(False)
398 |
399 | lim = plot.axes.get_xlim()
400 |
401 | self.form.xMin.setText(str(lim[0]))
402 | self.form.xMax.setText(str(lim[1]))
403 |
404 | else:
405 |
406 | self.form.xMin.setEnabled(True)
407 | self.form.xMax.setEnabled(True)
408 |
409 | try:
410 | xMin = float(self.form.xMin.text())
411 | except:
412 | xMin = plot.axes.get_xlim()[0]
413 | self.form.xMin.setText(str(xMin))
414 |
415 | try:
416 | xMax = float(self.form.xMax.text())
417 | except:
418 | xMax = plot.axes.get_xlim()[1]
419 | self.form.xMax.setText(str(xMax))
420 |
421 | for ax in axesList:
422 | ax.set_xlim((xMin, xMax))
423 |
424 | # Y axis
425 |
426 | if self.form.yAuto.isChecked():
427 |
428 | for ax in axesList:
429 | ax.set_autoscaley_on(True)
430 |
431 | self.form.yMin.setEnabled(False)
432 | self.form.yMax.setEnabled(False)
433 |
434 | lim = plot.axes.get_ylim()
435 |
436 | self.form.yMin.setText(str(lim[0]))
437 | self.form.yMax.setText(str(lim[1]))
438 |
439 | else:
440 |
441 | self.form.yMin.setEnabled(True)
442 | self.form.yMax.setEnabled(True)
443 |
444 | try:
445 | yMin = float(self.form.yMin.text())
446 | except:
447 | yMin = plot.axes.get_ylim()[0]
448 | self.form.yMin.setText(str(yMin))
449 |
450 | try:
451 | yMax = float(self.form.yMax.text())
452 | except:
453 | yMax = plot.axes.get_ylim()[1]
454 | self.form.yMax.setText(str(yMax))
455 |
456 | for ax in axesList:
457 | ax.set_ylim((yMin, yMax))
458 |
459 | plot.update()
460 |
461 | self.skip = False
462 |
463 |
464 | def onMdiArea ( self , subWin ):
465 |
466 | '''
467 | Executed when window is selected on mdi area.
468 |
469 | Keyword arguments:
470 | subWin -- Selected window.
471 | '''
472 |
473 | plot = Plot.getPlot()
474 |
475 | if plot != subWin:
476 | self.updateUI()
477 |
478 |
479 | def updateUI ( self ):
480 |
481 | '''
482 | Setup UI controls values if possible
483 | '''
484 |
485 | plot = Plot.getPlot()
486 |
487 | form = self.form
488 |
489 | form.newAxesButton.setEnabled(bool(plot))
490 | form.delAxesButton.setEnabled(bool(plot))
491 | form.axesIndex.setEnabled(bool(plot))
492 | form.allAxes.setEnabled(bool(plot))
493 | form.posXMin.setEnabled(bool(plot))
494 | form.posXMax.setEnabled(bool(plot))
495 | form.posYMin.setEnabled(bool(plot))
496 | form.posYMax.setEnabled(bool(plot))
497 | form.xAlign.setEnabled(bool(plot))
498 | form.yAlign.setEnabled(bool(plot))
499 | form.xOffset.setEnabled(bool(plot))
500 | form.yOffset.setEnabled(bool(plot))
501 | form.xAuto.setEnabled(bool(plot))
502 | form.yAuto.setEnabled(bool(plot))
503 | form.xMin.setEnabled(bool(plot))
504 | form.xMax.setEnabled(bool(plot))
505 | form.yMin.setEnabled(bool(plot))
506 | form.yMax.setEnabled(bool(plot))
507 |
508 | if not plot:
509 | form.axesIndex.setValue(0)
510 | return
511 |
512 | # Ensure that active axes is correct
513 |
514 | index = min(form.axesIndex.value(), len(plot.axesList) - 1)
515 | form.axesIndex.setValue(index)
516 |
517 | # Set dimensions
518 |
519 | ax = plot.axes
520 | bb = ax.get_position()
521 |
522 | form.posXMin.setValue(int(100 * bb.min[0]))
523 | form.posXMax.setValue(int(100 * bb.max[0]))
524 | form.posYMin.setValue(int(100 * bb.min[1]))
525 | form.posYMax.setValue(int(100 * bb.max[1]))
526 |
527 | # Set alignment and offset
528 |
529 | xPos = ax.xaxis.get_ticks_position()
530 | yPos = ax.yaxis.get_ticks_position()
531 |
532 | xOffset = ax.spines['bottom'].get_position()[1]
533 | yOffset = ax.spines['left'].get_position()[1]
534 |
535 | if xPos == 'bottom' or xPos == 'default':
536 | form.xAlign.setCurrentIndex(0)
537 | else:
538 | form.xAlign.setCurrentIndex(1)
539 |
540 | form.xOffset.setValue(xOffset)
541 |
542 | if yPos == 'left' or yPos == 'default':
543 | form.yAlign.setCurrentIndex(0)
544 | else:
545 | form.yAlign.setCurrentIndex(1)
546 |
547 | form.yOffset.setValue(yOffset)
548 |
549 | # Set scales
550 |
551 | if ax.get_autoscalex_on():
552 | form.xAuto.setChecked(True)
553 | form.xMin.setEnabled(False)
554 | form.xMax.setEnabled(False)
555 | else:
556 | form.xAuto.setChecked(False)
557 | form.xMin.setEnabled(True)
558 | form.xMax.setEnabled(True)
559 |
560 | lim = ax.get_xlim()
561 |
562 | form.xMin.setText(str(lim[0]))
563 | form.xMax.setText(str(lim[1]))
564 |
565 | if ax.get_autoscaley_on():
566 | form.yAuto.setChecked(True)
567 | form.yMin.setEnabled(False)
568 | form.yMax.setEnabled(False)
569 | else:
570 | form.yAuto.setChecked(False)
571 | form.yMin.setEnabled(True)
572 | form.yMax.setEnabled(True)
573 |
574 | lim = ax.get_ylim()
575 |
576 | form.yMin.setText(str(lim[0]))
577 | form.yMax.setText(str(lim[1]))
578 |
579 |
580 | def createTask ():
581 |
582 | panel = TaskPanel()
583 |
584 | Gui.Control.showDialog(panel)
585 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Addon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
157 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Grid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
162 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Save.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
159 |
--------------------------------------------------------------------------------
/freecad/plot/Resources/Icons/Labels.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
172 |
--------------------------------------------------------------------------------