FreeCAD / src /Mod /Fem /femviewprovider /view_post_histogram.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# ***************************************************************************
# * Copyright (c) 2025 Stefan Tröger <stefantroeger@gmx.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD FEM postprocessing line plot ViewProvider for the document object"
__author__ = "Stefan Tröger"
__url__ = "https://www.freecad.org"
## @package view_post_lineplot
# \ingroup FEM
# \brief view provider for post line plot object
import FreeCAD
import FreeCADGui
import Plot
from PySide import QtGui, QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import io
import numpy as np
import matplotlib as mpl
from packaging.version import Version
from vtkmodules.numpy_interface.dataset_adapter import VTKArray
from . import view_base_fempostextractors
from . import view_base_fempostvisualization
from femtaskpanels import task_post_histogram
from . import view_base_femobject
_GuiPropHelper = view_base_femobject._GuiPropHelper
class EditViewWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldViewEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
vobj = self._object.ViewObject
self.widget.Legend.setText(vobj.Legend)
self._post_dialog._enumPropertyToCombobox(vobj, "Hatch", self.widget.Hatch)
self._post_dialog._enumPropertyToCombobox(vobj, "LineStyle", self.widget.LineStyle)
self.widget.LineWidth.setValue(vobj.LineWidth)
self.widget.HatchDensity.setValue(vobj.HatchDensity)
# setup the color buttons (don't use FreeCADs color button, as this does not work in popups!)
self._setup_color_button(self.widget.BarColor, vobj.BarColor, self.barColorChanged)
self._setup_color_button(self.widget.LineColor, vobj.LineColor, self.lineColorChanged)
self.widget.Legend.editingFinished.connect(self.legendChanged)
self.widget.Hatch.activated.connect(self.hatchPatternChanged)
self.widget.LineStyle.activated.connect(self.lineStyleChanged)
self.widget.HatchDensity.valueChanged.connect(self.hatchDensityChanged)
self.widget.LineWidth.valueChanged.connect(self.lineWidthChanged)
# sometimes weird sizes occur with spinboxes
self.widget.HatchDensity.setMaximumHeight(self.widget.Hatch.sizeHint().height())
self.widget.LineWidth.setMaximumHeight(self.widget.LineStyle.sizeHint().height())
def _setup_color_button(self, button, fcColor, callback):
barColor = QtGui.QColor(*[v * 255 for v in fcColor])
icon_size = button.iconSize()
icon_size.setWidth(icon_size.width() * 2)
button.setIconSize(icon_size)
pixmap = QtGui.QPixmap(icon_size)
pixmap.fill(barColor)
button.setIcon(pixmap)
action = QtGui.QWidgetAction(button)
diag = QtGui.QColorDialog(barColor, parent=button)
diag.setOption(QtGui.QColorDialog.DontUseNativeDialog, True)
diag.accepted.connect(action.trigger)
diag.rejected.connect(action.trigger)
diag.colorSelected.connect(callback)
action.setDefaultWidget(diag)
button.addAction(action)
button.setPopupMode(QtGui.QToolButton.InstantPopup)
@QtCore.Slot(QtGui.QColor)
def lineColorChanged(self, color):
pixmap = QtGui.QPixmap(self.widget.LineColor.iconSize())
pixmap.fill(color)
self.widget.LineColor.setIcon(pixmap)
self._object.ViewObject.LineColor = color.getRgb()
@QtCore.Slot(QtGui.QColor)
def barColorChanged(self, color):
pixmap = QtGui.QPixmap(self.widget.BarColor.iconSize())
pixmap.fill(color)
self.widget.BarColor.setIcon(pixmap)
self._object.ViewObject.BarColor = color.getRgb()
@QtCore.Slot(float)
def lineWidthChanged(self, value):
self._object.ViewObject.LineWidth = value
@QtCore.Slot(float)
def hatchDensityChanged(self, value):
self._object.ViewObject.HatchDensity = value
@QtCore.Slot(int)
def hatchPatternChanged(self, index):
self._object.ViewObject.Hatch = index
@QtCore.Slot(int)
def lineStyleChanged(self, index):
self._object.ViewObject.LineStyle = index
@QtCore.Slot()
def legendChanged(self):
self._object.ViewObject.Legend = self.widget.Legend.text()
class EditFieldAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramFieldAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Extract.setChecked(self._object.ExtractFrames)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
self.widget.Extract.toggled.connect(self.extractionChanged)
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(bool)
def extractionChanged(self, extract):
self._object.ExtractFrames = extract
self._post_dialog._recompute()
class EditIndexAppWidget(QtGui.QWidget):
def __init__(self, obj, post_dialog):
super().__init__()
self._object = obj
self._post_dialog = post_dialog
# load the ui and set it up
self.widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/PostHistogramIndexAppEdit.ui"
)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.widget)
self.setLayout(layout)
self.__init_widget()
def __init_widget(self):
# set the other properties
self.widget.Index.setValue(self._object.Index)
self._post_dialog._enumPropertyToCombobox(self._object, "XField", self.widget.Field)
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self.widget.Index.valueChanged.connect(self.indexChanged)
self.widget.Field.activated.connect(self.fieldChanged)
self.widget.Component.activated.connect(self.componentChanged)
# sometimes weird sizes occur with spinboxes
self.widget.Index.setMaximumHeight(self.widget.Field.sizeHint().height())
@QtCore.Slot(int)
def fieldChanged(self, index):
self._object.XField = index
self._post_dialog._enumPropertyToCombobox(self._object, "XComponent", self.widget.Component)
self._post_dialog._recompute()
@QtCore.Slot(int)
def componentChanged(self, index):
self._object.XComponent = index
self._post_dialog._recompute()
@QtCore.Slot(int)
def indexChanged(self, value):
self._object.Index = value
self._post_dialog._recompute()
class VPPostHistogramFieldData(view_base_fempostextractors.VPPostExtractor):
"""
A View Provider for extraction of 1D field data specially for histograms
"""
def __init__(self, vobj):
super().__init__(vobj)
vobj.Proxy = self
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyString",
name="Legend",
group="HistogramPlot",
doc=QT_TRANSLATE_NOOP("FEM", "The name used in the plots legend"),
value="",
),
_GuiPropHelper(
type="App::PropertyColor",
name="BarColor",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"),
value=(0, 85, 255, 255),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="Hatch",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The hatch pattern drawn in the bar"),
value=["None", "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*"],
),
_GuiPropHelper(
type="App::PropertyIntegerConstraint",
name="HatchDensity",
group="HistogramBar",
doc=QT_TRANSLATE_NOOP("FEM", "The line width of the hatch)"),
value=(1, 1, 99, 1),
),
_GuiPropHelper(
type="App::PropertyColor",
name="LineColor",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP("FEM", "The color the data bin area is drawn with"),
value=(0, 0, 0, 1), # black
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="LineWidth",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP(
"FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"
),
value=(1, 0, 99, 0.1),
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LineStyle",
group="HistogramLine",
doc=QT_TRANSLATE_NOOP("FEM", "The style the line is drawn in"),
value=["None", "-", "--", "-.", ":"],
),
]
return super()._get_properties() + prop
def attach(self, vobj):
self.Object = vobj.Object
self.ViewObject = vobj
def getIcon(self):
return ":/icons/FEM_PostField.svg"
def get_app_edit_widget(self, post_dialog):
return EditFieldAppWidget(self.Object, post_dialog)
def get_view_edit_widget(self, post_dialog):
return EditViewWidget(self.Object, post_dialog)
def get_preview(self):
fig = mpl.pyplot.figure(figsize=(0.4, 0.2), dpi=500)
ax = mpl.pyplot.Axes(fig, [0.0, 0.0, 2, 1])
ax.set_axis_off()
fig.add_axes(ax)
kwargs = self.get_kw_args()
patch = mpl.patches.Rectangle(xy=(0, 0), width=2, height=1, **kwargs)
ax.add_patch(patch)
data = io.BytesIO()
mpl.pyplot.savefig(data, bbox_inches=0, transparent=True)
mpl.pyplot.close()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data.getvalue())
return (pixmap, self.ViewObject.Legend)
def get_kw_args(self):
# builds kw args from the properties
kwargs = {}
# colors need a workaround, some error occurs with rgba tuple
kwargs["edgecolor"] = self.ViewObject.LineColor
kwargs["facecolor"] = self.ViewObject.BarColor
kwargs["linestyle"] = self.ViewObject.LineStyle
kwargs["linewidth"] = self.ViewObject.LineWidth
if self.ViewObject.Hatch != "None":
kwargs["hatch"] = self.ViewObject.Hatch * self.ViewObject.HatchDensity
return kwargs
def get_default_color_property(self):
return "BarColor"
class VPPostHistogramIndexOverFrames(VPPostHistogramFieldData):
"""
A View Provider for extraction of 1D index over frames data
"""
def __init__(self, vobj):
super().__init__(vobj)
def getIcon(self):
return ":/icons/FEM_PostIndex.svg"
def get_app_edit_widget(self, post_dialog):
return EditIndexAppWidget(self.Object, post_dialog)
class VPPostHistogram(view_base_fempostvisualization.VPPostVisualization):
"""
A View Provider for Histogram plots
"""
def __init__(self, vobj):
super().__init__(vobj)
def _get_properties(self):
prop = [
_GuiPropHelper(
type="App::PropertyBool",
name="Cumulative",
group="Histogram",
doc=QT_TRANSLATE_NOOP(
"FEM", "If the bars should show the cumulative sum left to right"
),
value=False,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="Type",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The type of histogram plotted"),
value=["bar", "barstacked", "step", "stepfilled"],
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="BarWidth",
group="Histogram",
doc=QT_TRANSLATE_NOOP(
"FEM", "The width of the bar, between 0 and 1 (1 being without gaps)"
),
value=(0.9, 0, 1, 0.05),
),
_GuiPropHelper(
type="App::PropertyFloatConstraint",
name="HatchLineWidth",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The line width of all drawn hatch patterns"),
value=(1, 0, 99, 0.1),
),
_GuiPropHelper(
type="App::PropertyInteger",
name="Bins",
group="Histogram",
doc=QT_TRANSLATE_NOOP("FEM", "The number of bins the data is split into"),
value=10,
),
_GuiPropHelper(
type="App::PropertyString",
name="Title",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The histogram plot title"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="XLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram X axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyString",
name="YLabel",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "The label shown for the histogram Y axis"),
value="",
),
_GuiPropHelper(
type="App::PropertyBool",
name="Legend",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=True,
),
_GuiPropHelper(
type="App::PropertyEnumeration",
name="LegendLocation",
group="Plot",
doc=QT_TRANSLATE_NOOP("FEM", "Determines if the legend is plotted"),
value=[
"best",
"upper right",
"upper left",
"lower left",
"lower right",
"right",
"center left",
"center right",
"lower center",
"upper center",
"center",
],
),
]
return prop
def getIcon(self):
return ":/icons/FEM_PostHistogram.svg"
def setEdit(self, vobj, mode):
# build up the task panel
taskd = task_post_histogram._TaskPanel(vobj)
# show it
FreeCADGui.Control.showDialog(taskd)
return True
def show_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
main = FreeCADGui.getMainWindow()
self._plot = Plot.Plot()
self._plot.setWindowTitle(self.Object.Label)
self._plot.setParent(main)
self._plot.setWindowFlags(QtGui.Qt.Dialog)
self._plot.resize(main.size().height() / 2, main.size().height() / 3) # keep it square
self.update_visualization()
self._plot.show()
def get_kw_args(self, obj):
view = obj.ViewObject
if not view or not hasattr(view, "Proxy"):
return {}
if not hasattr(view.Proxy, "get_kw_args"):
return {}
return view.Proxy.get_kw_args()
def update_visualization(self):
if not hasattr(self, "_plot") or not self._plot:
return
self._plot.axes.clear()
bins = self.ViewObject.Bins
# we do not iterate the table, but iterate the children. This makes it possible
# to attribute the correct styles
full_args = []
full_data = []
labels = []
for child in self.Object.Group:
table = child.Table
kwargs = self.get_kw_args(child)
# iterate over the table and plot all
color_factor = np.linspace(1, 0.5, table.GetNumberOfColumns())
legend_multiframe = table.GetNumberOfColumns() > 1
for i in range(table.GetNumberOfColumns()):
# add the kw args, with some slide change over color for multiple frames
args = kwargs.copy()
for key in kwargs:
if "color" in key:
value = np.array(kwargs[key]) * color_factor[i]
args[key] = mpl.colors.to_hex(value)
full_args.append(args)
data = VTKArray(table.GetColumn(i))
full_data.append(data)
# legend labels
if child.ViewObject.Legend:
if not legend_multiframe:
labels.append(child.ViewObject.Legend)
else:
postfix = table.GetColumnName(i).split("-")[-1]
labels.append(child.ViewObject.Legend + " - " + postfix)
else:
legend_prefix = ""
if len(self.Object.Group) > 1:
legend_prefix = child.Source.Label + ": "
labels.append(legend_prefix + table.GetColumnName(i))
args = {}
args["rwidth"] = self.ViewObject.BarWidth
args["cumulative"] = self.ViewObject.Cumulative
args["histtype"] = self.ViewObject.Type
args["label"] = labels
if Version(mpl.__version__) >= Version("3.10.0"):
args["hatch_linewidth"] = self.ViewObject.HatchLineWidth
n, b, patches = self._plot.axes.hist(full_data, bins, **args)
# set the patches view properties.
if len(full_args) == 1:
for patch in patches:
patch.set(**full_args[0])
elif len(full_args) > 1:
for i, args in enumerate(full_args):
for patch in patches[i]:
patch.set(**full_args[i])
# axes decoration
if self.ViewObject.Title:
self._plot.axes.set_title(self.ViewObject.Title)
if self.ViewObject.XLabel:
self._plot.axes.set_xlabel(self.ViewObject.XLabel)
if self.ViewObject.YLabel:
self._plot.axes.set_ylabel(self.ViewObject.YLabel)
if self.ViewObject.Legend and labels:
self._plot.axes.legend(loc=self.ViewObject.LegendLocation)
self._plot.update()
def get_next_default_color(self):
# we use the next color in order. We do not check (yet) if this
# color is already taken
i = len(self.Object.Group)
cmap = mpl.pyplot.get_cmap("tab10")
return cmap(i)