-
Notifications
You must be signed in to change notification settings - Fork 3
Add QtGraphs-based analysis page to AdvancedPy example #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
2e143e2
f13672e
7802c18
5190d00
eaa3b8e
ddaad32
53b2437
3dd99ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // SPDX-FileCopyrightText: 2024 EasyApp contributors | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| // © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp> | ||
|
|
||
| pragma Singleton | ||
|
|
||
| import QtQuick | ||
| import QtGraphs | ||
|
|
||
| import Gui.Globals as Globals | ||
|
|
||
|
|
||
| QtObject { | ||
|
|
||
| property int dataSize: 50 | ||
| property var axesRanges: { | ||
| "xmin": 0.0, | ||
| "xmax": 180.0, | ||
| "ymin": 0.0, | ||
| "ymax": 100.0, | ||
| } | ||
|
|
||
| signal dataPointsChanged(var points) | ||
|
|
||
| function generateData() { | ||
| console.debug(`* Generating ${dataSize} data points...`) | ||
| const xmin = axesRanges.xmin | ||
| const xmax = axesRanges.xmax | ||
| const ymin = axesRanges.ymin | ||
| const ymax = axesRanges.ymax | ||
|
|
||
| const pointCount = Math.max(1, dataSize) | ||
| const stepSize = pointCount > 1 ? (xmax - xmin) / (pointCount - 1) : 0 | ||
|
|
||
| let dataPoints = [] | ||
| for (let i = 0; i < pointCount; i++) { | ||
| const x = xmin + i * stepSize | ||
| const y = ymin + Math.random() * (ymax - ymin) | ||
| dataPoints.push(Qt.point(x, y)) | ||
| } | ||
| console.debug(" Data generation completed.") | ||
|
|
||
| console.debug(`* Sending ${pointCount} data points to series...`) | ||
| dataPointsChanged(dataPoints) | ||
| console.debug(" Data update signal emitted.") | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| module MockQml | ||
|
|
||
| singleton Project Project.qml | ||
| singleton Analysis Analysis.qml | ||
| singleton Report Report.qml | ||
| singleton Status Status.qml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # SPDX-FileCopyrightText: 2024 EasyApp contributors | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp> | ||
|
|
||
| import numpy as np | ||
| from PySide6.QtCore import QObject, Signal, Slot, Property, QPointF | ||
|
|
||
| from EasyApp.Logic.Logging import console | ||
|
|
||
|
|
||
| class Analysis(QObject): | ||
| """ | ||
| Backend object that generates synthetic diffraction-like data | ||
| and exposes it to QML as a list of QPointF values plus axis ranges. | ||
| """ | ||
|
|
||
| # Signals | ||
| dataSizeChanged = Signal() | ||
| dataPointsChanged = Signal("QVariantList") # Emitted with list<QPointF> | ||
| axesRangesChanged = Signal() # Emitted when range dict updates | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
|
|
||
| self._dataSize = 10000 | ||
| self._axesRanges = { | ||
| "xmin": 0.0, | ||
| "xmax": 180.0, | ||
| "ymin": 0.0, | ||
| "ymax": 100.0, | ||
| } | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # QML-accessible Properties | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @Property(int, notify=dataSizeChanged) | ||
| def dataSize(self): | ||
| """Number of X/Y data points to generate.""" | ||
| return self._dataSize | ||
|
|
||
| @dataSize.setter | ||
| def dataSize(self, value): | ||
| value = int(value) | ||
| if self._dataSize == value: | ||
| return | ||
| self._dataSize = value | ||
| self.dataSizeChanged.emit() | ||
|
|
||
| @Property("QVariantMap", notify=axesRangesChanged) | ||
| def axesRanges(self): | ||
| """ | ||
| Axis ranges used by the graph: | ||
| { "xmin": float, "xmax": float, "ymin": float, "ymax": float } | ||
| Access in QML using: axisX.min: analysis.axesRanges["xmin"] | ||
| """ | ||
| return self._axesRanges | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Public Slot Called from QML | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @Slot() | ||
| def generateData(self): | ||
| """Generate new synthetic data and notify QML.""" | ||
| console.debug(f"* Generating {self.dataSize} data points...") | ||
| x, y = self._generate_data(n_points=self.dataSize) | ||
| console.debug(" Data generation completed.") | ||
|
Comment on lines
+67
to
+68
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No exception handling on results from |
||
|
|
||
| console.debug(f"* Converting and sending {self.dataSize} data points to series...") | ||
| self.dataPointsChanged.emit(self._ndarrays_to_qpoints(x, y)) | ||
| console.debug(" Data update signal emitted.") | ||
|
|
||
| self._updateAxesRanges(x.min(), x.max(), y.min(), y.max()) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Internal Helpers | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def _updateAxesRanges(self, xmin, xmax, ymin, ymax): | ||
| """Store axis ranges and notify QML.""" | ||
| vmargin = 10.0 | ||
| self._axesRanges["xmin"] = float(xmin) | ||
| self._axesRanges["xmax"] = float(xmax) | ||
| self._axesRanges["ymin"] = max(0, float(ymin) - vmargin) | ||
| self._axesRanges["ymax"] = float(ymax) + vmargin | ||
| self.axesRangesChanged.emit() | ||
|
|
||
| @staticmethod | ||
| def _ndarrays_to_qpoints(x: np.ndarray, y: np.ndarray): | ||
| """ | ||
| Convert NumPy X/Y arrays to list[QPointF]. | ||
| Uses memoryview to avoid Python float conversions inside numpy. | ||
| """ | ||
| mvx = memoryview(x) | ||
| mvy = memoryview(y) | ||
| return [QPointF(xi, yi) for xi, yi in zip(mvx, mvy)] | ||
|
Comment on lines
+95
to
+97
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
>>> a = np.array([1.0, 2.0])
>>> for x in memoryview(a):
... print(type(x))
<class 'float'>
<class 'float'> |
||
|
|
||
| @staticmethod | ||
| def _generate_data( | ||
| n_points=2000, | ||
| n_peaks=100, | ||
| x_range=(0.0, 180.0), | ||
| intensity_range=(0, 100), | ||
| width_range=(0.05, 0.5), | ||
| noise_level=1.0, | ||
| background=20.0, | ||
| ): | ||
| """ | ||
| Generate synthetic diffraction-like pattern from sum of random Gaussians. | ||
| Returns (x, y) NumPy arrays. | ||
| """ | ||
| # Sample x grid | ||
| x = np.linspace(*x_range, n_points) | ||
| y = np.zeros_like(x) | ||
|
|
||
| # Random peak positions, intensities, widths | ||
| positions = np.random.uniform(*x_range, n_peaks) | ||
| amplitudes = np.random.uniform(*intensity_range, n_peaks) | ||
| widths = np.random.uniform(*width_range, n_peaks) | ||
|
|
||
| # Gaussian peak contributions | ||
| for pos, amp, width in zip(positions, amplitudes, widths): | ||
| y += amp * np.exp(-0.5 * ((x - pos) / width) ** 2) | ||
|
|
||
| # Noise | ||
| y += np.random.normal(scale=noise_level, size=n_points) | ||
|
|
||
| # Background | ||
| y += background | ||
|
|
||
| return x, y | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // SPDX-FileCopyrightText: 2024 EasyApp contributors | ||
| // SPDX-License-Identifier: BSD-3-Clause | ||
| // © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp> | ||
|
|
||
| import QtQuick | ||
| import QtQuick.Controls | ||
|
|
||
| import EasyApp.Gui.Style as EaStyle | ||
| import EasyApp.Gui.Globals as EaGlobals | ||
| import EasyApp.Gui.Elements as EaElements | ||
| import EasyApp.Gui.Components as EaComponents | ||
|
|
||
| import Gui.Globals as Globals | ||
|
|
||
|
|
||
| EaComponents.ContentPage { | ||
|
|
||
| mainView: EaComponents.MainContent { | ||
| tabs: [ | ||
| EaElements.TabButton { text: qsTr('Chart') } | ||
| ] | ||
|
|
||
| items: [ | ||
| Loader { source: 'MainArea/Chart.qml' } | ||
| ] | ||
| } | ||
|
|
||
| sideBar: EaComponents.SideBar { | ||
| tabs: [ | ||
| EaElements.TabButton { text: qsTr('Basic controls') } | ||
| ] | ||
|
|
||
| items: [ | ||
| Loader { source: 'Sidebar/Basic/Layout.qml' } | ||
| ] | ||
|
|
||
| continueButton.text: qsTr('Continue') | ||
|
|
||
| continueButton.onClicked: { | ||
| console.debug(`Clicking '${continueButton.text}' button ::: ${this}`) | ||
| Globals.References.applicationWindow.appBarCentralTabs.summaryButton.enabled = true | ||
| Globals.References.applicationWindow.appBarCentralTabs.summaryButton.toggle() | ||
| } | ||
| } | ||
|
|
||
| Component.onCompleted: console.debug(`Analysis page loaded ::: ${this}`) | ||
| Component.onDestruction: console.debug(`Analysis page destroyed ::: ${this}`) | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the default
dataSizein correspondinganalysis.pyis set to 10000. Why the difference?