├── 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 | 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | image/svg+xml 101 | 102 | 103 | 104 | 105 | Jose Luis Cercós Pita ( sanguinariojoe ) 106 | 107 | 108 | Series 109 | 2012-11-02 110 | FreeCAD/src/Mod/Plot/resources/icons/Series.svg 111 | http://www.freecadweb.org/wiki/index.php?title=Artwork 112 | 113 | 114 | 115 | 116 | Alexander Gryson ( agryson ) 117 | 118 | 119 | 121 | 122 | 124 | 126 | 128 | 130 | 132 | 134 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 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 | 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | image/svg+xml 131 | 132 | 133 | 134 | 135 | Jose Luis Cercós Pita ( sanguinariojoe ) 136 | 137 | 138 | Axes 139 | 2012-11-02 140 | FreeCAD/src/Mod/Plot/resources/icons/Axes.svg 141 | http://www.freecadweb.org/wiki/index.php?title=Artwork 142 | 143 | 144 | 145 | 146 | Alexander Gryson ( agryson ) 147 | 148 | 149 | 151 | 152 | 154 | 156 | 158 | 160 | 162 | 164 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 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 | 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | image/svg+xml 79 | 80 | 81 | 82 | 83 | Jose Luis Cercós Pita ( sanguinariojoe ) 84 | 85 | 86 | Icon 87 | 2012-11-02 88 | FreeCAD/src/Mod/Plot/resources/icons/Icon.svg 89 | http://www.freecadweb.org/wiki/index.php?title=Artwork 90 | 91 | 92 | 93 | 94 | Alexander Gryson ( agryson ) 95 | 96 | 97 | 99 | 100 | 102 | 104 | 106 | 108 | 110 | 112 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /freecad/plot/Resources/Icons/Grid.svg: -------------------------------------------------------------------------------- 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | image/svg+xml 97 | 98 | 99 | 100 | 101 | Jose Luis Cercós Pita ( sanguinariojoe ) 102 | 103 | 104 | Grid 105 | 2012-11-02 106 | FreeCAD/src/Mod/Plot/resources/icons/Grid.svg 107 | http://www.freecadweb.org/wiki/index.php?title=Artwork 108 | 109 | 110 | 111 | 112 | Alexander Gryson ( agryson ) 113 | 114 | 115 | 117 | 118 | 120 | 122 | 124 | 126 | 128 | 130 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /freecad/plot/Resources/Icons/Save.svg: -------------------------------------------------------------------------------- 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | image/svg+xml 69 | 70 | 71 | 72 | 73 | Jose Luis Cercós Pita ( sanguinariojoe ) 74 | 75 | 76 | Save 77 | 2012-11-02 78 | FreeCAD/src/Mod/Plot/resources/icons/Save.svg 79 | http://www.freecadweb.org/wiki/index.php?title=Artwork 80 | 81 | 82 | 83 | 84 | Alexander Gryson ( agryson ) 85 | 86 | 87 | 89 | 90 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /freecad/plot/Resources/Icons/Labels.svg: -------------------------------------------------------------------------------- 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | image/svg+xml 89 | 90 | 91 | 92 | 93 | Jose Luis Cercós Pita ( sanguinariojoe ) 94 | 95 | 96 | Labels 97 | 2012-11-02 98 | FreeCAD/src/Mod/Plot/resources/icons/Labels.svg 99 | http://www.freecadweb.org/wiki/index.php?title=Artwork 100 | 101 | 102 | 103 | 104 | Alexander Gryson ( agryson ) 105 | 106 | 107 | 109 | 110 | 112 | 114 | 116 | 118 | 120 | 122 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | --------------------------------------------------------------------------------