fieldview 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fieldview-0.1.0/.gitignore +158 -0
- fieldview-0.1.0/LICENSE +21 -0
- fieldview-0.1.0/PKG-INFO +121 -0
- fieldview-0.1.0/README.md +109 -0
- fieldview-0.1.0/fieldview/__init__.py +0 -0
- fieldview-0.1.0/fieldview/core/__init__.py +0 -0
- fieldview-0.1.0/fieldview/core/data_container.py +158 -0
- fieldview-0.1.0/fieldview/layers/__init__.py +0 -0
- fieldview-0.1.0/fieldview/layers/data_layer.py +89 -0
- fieldview-0.1.0/fieldview/layers/heatmap_layer.py +332 -0
- fieldview-0.1.0/fieldview/layers/layer.py +31 -0
- fieldview-0.1.0/fieldview/layers/pin_layer.py +37 -0
- fieldview-0.1.0/fieldview/layers/svg_layer.py +33 -0
- fieldview-0.1.0/fieldview/layers/text_layer.py +224 -0
- fieldview-0.1.0/fieldview/rendering/__init__.py +0 -0
- fieldview-0.1.0/fieldview/rendering/colormaps.py +97 -0
- fieldview-0.1.0/fieldview/resources/fonts/JetBrainsMono-Regular.ttf +0 -0
- fieldview-0.1.0/fieldview/ui/__init__.py +0 -0
- fieldview-0.1.0/pyproject.toml +36 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
share/python-wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
MANIFEST
|
|
24
|
+
|
|
25
|
+
# PyInstaller
|
|
26
|
+
# Usually these files are written by a python script from a template
|
|
27
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
cover/
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Django stuff:
|
|
55
|
+
*.log
|
|
56
|
+
local_settings.py
|
|
57
|
+
db.sqlite3
|
|
58
|
+
db.sqlite3-journal
|
|
59
|
+
|
|
60
|
+
# Flask stuff:
|
|
61
|
+
instance/
|
|
62
|
+
.webassets-cache
|
|
63
|
+
|
|
64
|
+
# Scrapy stuff:
|
|
65
|
+
.scrapy
|
|
66
|
+
|
|
67
|
+
# Sphinx documentation
|
|
68
|
+
docs/_build/
|
|
69
|
+
|
|
70
|
+
# PyBuilder
|
|
71
|
+
.pybuilder/
|
|
72
|
+
|
|
73
|
+
# Jupyter Notebook
|
|
74
|
+
.ipynb_checkpoints
|
|
75
|
+
|
|
76
|
+
# IPython
|
|
77
|
+
profile_default/
|
|
78
|
+
ipython_config.py
|
|
79
|
+
|
|
80
|
+
# pyenv
|
|
81
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
82
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
83
|
+
# .python-version
|
|
84
|
+
|
|
85
|
+
# pipenv
|
|
86
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
87
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
88
|
+
# with no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
89
|
+
# install all needed dependencies.
|
|
90
|
+
#Pipfile.lock
|
|
91
|
+
|
|
92
|
+
# poetry
|
|
93
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
94
|
+
# This is especially recommended for binary dependencies to ensure reproducible builds.
|
|
95
|
+
#poetry.lock
|
|
96
|
+
|
|
97
|
+
# pdm
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
99
|
+
#pdm.lock
|
|
100
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
101
|
+
# in version control.
|
|
102
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
103
|
+
.pdm.toml
|
|
104
|
+
|
|
105
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
106
|
+
__pypackages__/
|
|
107
|
+
|
|
108
|
+
# Celery stuff
|
|
109
|
+
celerybeat-schedule
|
|
110
|
+
celerybeat.pid
|
|
111
|
+
|
|
112
|
+
# SageMath parsed files
|
|
113
|
+
*.sage.py
|
|
114
|
+
|
|
115
|
+
# Environments
|
|
116
|
+
.env
|
|
117
|
+
.venv
|
|
118
|
+
env/
|
|
119
|
+
venv/
|
|
120
|
+
ENV/
|
|
121
|
+
env.bak/
|
|
122
|
+
venv.bak/
|
|
123
|
+
|
|
124
|
+
# Spyder project settings
|
|
125
|
+
.spyderproject
|
|
126
|
+
.spyproject
|
|
127
|
+
|
|
128
|
+
# Rope project settings
|
|
129
|
+
.ropeproject
|
|
130
|
+
|
|
131
|
+
# mkdocs documentation
|
|
132
|
+
/site
|
|
133
|
+
|
|
134
|
+
# mypy
|
|
135
|
+
.mypy_cache/
|
|
136
|
+
.dmypy.json
|
|
137
|
+
dmypy.json
|
|
138
|
+
|
|
139
|
+
# Pyre type checker
|
|
140
|
+
.pyre/
|
|
141
|
+
|
|
142
|
+
# pytype static type analyzer
|
|
143
|
+
.pytype/
|
|
144
|
+
|
|
145
|
+
# Cython debug symbols
|
|
146
|
+
cython_debug/
|
|
147
|
+
|
|
148
|
+
# PyCharm
|
|
149
|
+
.idea/
|
|
150
|
+
|
|
151
|
+
# VS Code
|
|
152
|
+
.vscode/
|
|
153
|
+
|
|
154
|
+
# MacOS
|
|
155
|
+
.DS_Store
|
|
156
|
+
|
|
157
|
+
# Generated demo files
|
|
158
|
+
dummy_data.csv
|
fieldview-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 FieldView Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fieldview-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fieldview
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python + Qt based 2D data visualization solution for irregular data points
|
|
5
|
+
Author-email: FieldView Team <dev@fieldview.org>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: numpy>=1.24.1
|
|
9
|
+
Requires-Dist: pyside6>=6.6.0
|
|
10
|
+
Requires-Dist: scipy>=1.10.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# FieldView
|
|
14
|
+
|
|
15
|
+
**FieldView** is a high-performance Python + Qt (PySide6) library for 2D data visualization, specifically designed for handling irregular data points. It provides a robust rendering engine for heatmaps, markers, and text labels with minimal external dependencies.
|
|
16
|
+
|
|
17
|
+
<img src="assets/quick_start.png" alt="Quick Start" width="480">
|
|
18
|
+
|
|
19
|
+
## Key Features
|
|
20
|
+
|
|
21
|
+
* **Fast Heatmap Rendering**: Hybrid RBF (Radial Basis Function) interpolation for high-quality visualization with real-time performance optimization.
|
|
22
|
+
* **Irregular Data Support**: Native handling of non-grid data points.
|
|
23
|
+
* **Polygon Masking**: Support for arbitrary boundary shapes (Polygon, Circle, Rectangle) to clip heatmaps.
|
|
24
|
+
* **Layer System**: Modular architecture with support for:
|
|
25
|
+
* **HeatmapLayer**: Color-based data visualization.
|
|
26
|
+
* **ValueLayer/LabelLayer**: Text rendering with collision avoidance.
|
|
27
|
+
* **PinLayer**: Marker placement.
|
|
28
|
+
* **SvgLayer**: Background floor plans or overlays.
|
|
29
|
+
* **Minimal Dependencies**: Built on `numpy`, `scipy`, and `PySide6`.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install fieldview
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
*Note: Requires Python 3.10+*
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Here is a minimal example to get a heatmap up and running:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import sys
|
|
45
|
+
import os
|
|
46
|
+
import numpy as np
|
|
47
|
+
from PySide6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene
|
|
48
|
+
from PySide6.QtCore import Qt
|
|
49
|
+
from fieldview.core.data_container import DataContainer
|
|
50
|
+
from fieldview.layers.heatmap_layer import HeatmapLayer
|
|
51
|
+
from fieldview.layers.text_layer import ValueLayer
|
|
52
|
+
from fieldview.layers.svg_layer import SvgLayer
|
|
53
|
+
from fieldview.layers.pin_layer import PinLayer
|
|
54
|
+
|
|
55
|
+
app = QApplication(sys.argv)
|
|
56
|
+
|
|
57
|
+
# 1. Setup Data
|
|
58
|
+
data = DataContainer()
|
|
59
|
+
np.random.seed(44)
|
|
60
|
+
points = (np.random.rand(20, 2) - 0.5) * 300
|
|
61
|
+
values = np.random.rand(20) * 100
|
|
62
|
+
data.set_data(points, values)
|
|
63
|
+
|
|
64
|
+
# 2. Create Scene & Layers
|
|
65
|
+
scene = QGraphicsScene()
|
|
66
|
+
|
|
67
|
+
# SVG Layer (Background)
|
|
68
|
+
# Assuming floorplan.svg exists in current dir or provide path
|
|
69
|
+
svg_layer = SvgLayer()
|
|
70
|
+
svg_layer.load_svg("examples/floorplan.svg")
|
|
71
|
+
svg_layer.setZValue(0)
|
|
72
|
+
scene.addItem(svg_layer)
|
|
73
|
+
|
|
74
|
+
# Heatmap Layer
|
|
75
|
+
heatmap = HeatmapLayer(data)
|
|
76
|
+
heatmap.setOpacity(0.6)
|
|
77
|
+
heatmap.setZValue(1)
|
|
78
|
+
heatmap.set_boundary_shape(svg_layer._bounding_rect)
|
|
79
|
+
scene.addItem(heatmap)
|
|
80
|
+
|
|
81
|
+
# Pin Layer
|
|
82
|
+
pin_layer = PinLayer(data)
|
|
83
|
+
pin_layer.setZValue(2)
|
|
84
|
+
scene.addItem(pin_layer)
|
|
85
|
+
|
|
86
|
+
# Value Layer
|
|
87
|
+
values_layer = ValueLayer(data)
|
|
88
|
+
values_layer.setZValue(3)
|
|
89
|
+
scene.addItem(values_layer)
|
|
90
|
+
|
|
91
|
+
# 3. Setup View
|
|
92
|
+
view = QGraphicsView(scene)
|
|
93
|
+
view.resize(800, 600)
|
|
94
|
+
view.show()
|
|
95
|
+
|
|
96
|
+
# Ensure content is visible
|
|
97
|
+
scene.setSceneRect(scene.itemsBoundingRect())
|
|
98
|
+
view.fitInView(scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
|
99
|
+
|
|
100
|
+
sys.exit(app.exec())
|
|
101
|
+
```
|
|
102
|
+
≈
|
|
103
|
+
|
|
104
|
+
## Running the Demo
|
|
105
|
+
|
|
106
|
+
To see all features in action, including the property editor and real-time interaction:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Clone the repository
|
|
110
|
+
git clone https://github.com/yourusername/fieldview.git
|
|
111
|
+
cd fieldview
|
|
112
|
+
|
|
113
|
+
# Run the demo using uv (recommended)
|
|
114
|
+
uv run examples/demo.py
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
<img src="assets/demo.gif" alt="Demo" width="800">
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT License
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# FieldView
|
|
2
|
+
|
|
3
|
+
**FieldView** is a high-performance Python + Qt (PySide6) library for 2D data visualization, specifically designed for handling irregular data points. It provides a robust rendering engine for heatmaps, markers, and text labels with minimal external dependencies.
|
|
4
|
+
|
|
5
|
+
<img src="assets/quick_start.png" alt="Quick Start" width="480">
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
* **Fast Heatmap Rendering**: Hybrid RBF (Radial Basis Function) interpolation for high-quality visualization with real-time performance optimization.
|
|
10
|
+
* **Irregular Data Support**: Native handling of non-grid data points.
|
|
11
|
+
* **Polygon Masking**: Support for arbitrary boundary shapes (Polygon, Circle, Rectangle) to clip heatmaps.
|
|
12
|
+
* **Layer System**: Modular architecture with support for:
|
|
13
|
+
* **HeatmapLayer**: Color-based data visualization.
|
|
14
|
+
* **ValueLayer/LabelLayer**: Text rendering with collision avoidance.
|
|
15
|
+
* **PinLayer**: Marker placement.
|
|
16
|
+
* **SvgLayer**: Background floor plans or overlays.
|
|
17
|
+
* **Minimal Dependencies**: Built on `numpy`, `scipy`, and `PySide6`.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install fieldview
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
*Note: Requires Python 3.10+*
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
Here is a minimal example to get a heatmap up and running:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import sys
|
|
33
|
+
import os
|
|
34
|
+
import numpy as np
|
|
35
|
+
from PySide6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene
|
|
36
|
+
from PySide6.QtCore import Qt
|
|
37
|
+
from fieldview.core.data_container import DataContainer
|
|
38
|
+
from fieldview.layers.heatmap_layer import HeatmapLayer
|
|
39
|
+
from fieldview.layers.text_layer import ValueLayer
|
|
40
|
+
from fieldview.layers.svg_layer import SvgLayer
|
|
41
|
+
from fieldview.layers.pin_layer import PinLayer
|
|
42
|
+
|
|
43
|
+
app = QApplication(sys.argv)
|
|
44
|
+
|
|
45
|
+
# 1. Setup Data
|
|
46
|
+
data = DataContainer()
|
|
47
|
+
np.random.seed(44)
|
|
48
|
+
points = (np.random.rand(20, 2) - 0.5) * 300
|
|
49
|
+
values = np.random.rand(20) * 100
|
|
50
|
+
data.set_data(points, values)
|
|
51
|
+
|
|
52
|
+
# 2. Create Scene & Layers
|
|
53
|
+
scene = QGraphicsScene()
|
|
54
|
+
|
|
55
|
+
# SVG Layer (Background)
|
|
56
|
+
# Assuming floorplan.svg exists in current dir or provide path
|
|
57
|
+
svg_layer = SvgLayer()
|
|
58
|
+
svg_layer.load_svg("examples/floorplan.svg")
|
|
59
|
+
svg_layer.setZValue(0)
|
|
60
|
+
scene.addItem(svg_layer)
|
|
61
|
+
|
|
62
|
+
# Heatmap Layer
|
|
63
|
+
heatmap = HeatmapLayer(data)
|
|
64
|
+
heatmap.setOpacity(0.6)
|
|
65
|
+
heatmap.setZValue(1)
|
|
66
|
+
heatmap.set_boundary_shape(svg_layer._bounding_rect)
|
|
67
|
+
scene.addItem(heatmap)
|
|
68
|
+
|
|
69
|
+
# Pin Layer
|
|
70
|
+
pin_layer = PinLayer(data)
|
|
71
|
+
pin_layer.setZValue(2)
|
|
72
|
+
scene.addItem(pin_layer)
|
|
73
|
+
|
|
74
|
+
# Value Layer
|
|
75
|
+
values_layer = ValueLayer(data)
|
|
76
|
+
values_layer.setZValue(3)
|
|
77
|
+
scene.addItem(values_layer)
|
|
78
|
+
|
|
79
|
+
# 3. Setup View
|
|
80
|
+
view = QGraphicsView(scene)
|
|
81
|
+
view.resize(800, 600)
|
|
82
|
+
view.show()
|
|
83
|
+
|
|
84
|
+
# Ensure content is visible
|
|
85
|
+
scene.setSceneRect(scene.itemsBoundingRect())
|
|
86
|
+
view.fitInView(scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
|
87
|
+
|
|
88
|
+
sys.exit(app.exec())
|
|
89
|
+
```
|
|
90
|
+
≈
|
|
91
|
+
|
|
92
|
+
## Running the Demo
|
|
93
|
+
|
|
94
|
+
To see all features in action, including the property editor and real-time interaction:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Clone the repository
|
|
98
|
+
git clone https://github.com/yourusername/fieldview.git
|
|
99
|
+
cd fieldview
|
|
100
|
+
|
|
101
|
+
# Run the demo using uv (recommended)
|
|
102
|
+
uv run examples/demo.py
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
<img src="assets/demo.gif" alt="Demo" width="800">
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT License
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PySide6.QtCore import QObject, Signal
|
|
3
|
+
|
|
4
|
+
class DataContainer(QObject):
|
|
5
|
+
"""
|
|
6
|
+
Manages the core data (points and values) for the FieldView library.
|
|
7
|
+
Emits signals when data changes.
|
|
8
|
+
"""
|
|
9
|
+
dataChanged = Signal()
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self._points = np.empty((0, 2), dtype=float)
|
|
14
|
+
self._values = np.empty((0,), dtype=float)
|
|
15
|
+
self._labels = []
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def points(self):
|
|
19
|
+
return self._points
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def values(self):
|
|
23
|
+
return self._values
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def labels(self):
|
|
27
|
+
return self._labels
|
|
28
|
+
|
|
29
|
+
def set_data(self, points, values, labels=None):
|
|
30
|
+
"""
|
|
31
|
+
Sets the data points, values, and optional labels.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
points (np.ndarray): Nx2 array of (x, y) coordinates.
|
|
35
|
+
values (np.ndarray): N array of values.
|
|
36
|
+
labels (list): Optional list of N strings.
|
|
37
|
+
"""
|
|
38
|
+
points = np.array(points)
|
|
39
|
+
values = np.array(values)
|
|
40
|
+
|
|
41
|
+
if points.ndim != 2 or points.shape[1] != 2:
|
|
42
|
+
raise ValueError("Points must be an Nx2 array.")
|
|
43
|
+
if values.ndim != 1:
|
|
44
|
+
raise ValueError("Values must be a 1D array.")
|
|
45
|
+
if len(points) != len(values):
|
|
46
|
+
raise ValueError("Points and values must have the same length.")
|
|
47
|
+
|
|
48
|
+
if labels is None:
|
|
49
|
+
labels = [""] * len(points)
|
|
50
|
+
elif len(labels) != len(points):
|
|
51
|
+
raise ValueError("Labels must have the same length as points.")
|
|
52
|
+
|
|
53
|
+
self._points = points
|
|
54
|
+
self._values = values
|
|
55
|
+
self._labels = list(labels)
|
|
56
|
+
self.dataChanged.emit()
|
|
57
|
+
|
|
58
|
+
def add_points(self, points, values, labels=None):
|
|
59
|
+
"""
|
|
60
|
+
Adds new points, values, and optional labels.
|
|
61
|
+
"""
|
|
62
|
+
points = np.array(points)
|
|
63
|
+
values = np.array(values)
|
|
64
|
+
|
|
65
|
+
if len(points) == 0:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if labels is None:
|
|
69
|
+
labels = [""] * len(points)
|
|
70
|
+
elif len(labels) != len(points):
|
|
71
|
+
raise ValueError("Labels must have the same length as points.")
|
|
72
|
+
|
|
73
|
+
if self._points.shape[0] == 0:
|
|
74
|
+
self.set_data(points, values, labels)
|
|
75
|
+
else:
|
|
76
|
+
self._points = np.vstack((self._points, points))
|
|
77
|
+
self._values = np.concatenate((self._values, values))
|
|
78
|
+
self._labels.extend(labels)
|
|
79
|
+
self.dataChanged.emit()
|
|
80
|
+
|
|
81
|
+
def update_point(self, index, value=None, point=None, label=None):
|
|
82
|
+
"""
|
|
83
|
+
Updates the value, coordinate, or label of a specific point.
|
|
84
|
+
"""
|
|
85
|
+
if index < 0 or index >= len(self._points):
|
|
86
|
+
raise IndexError("Point index out of range.")
|
|
87
|
+
|
|
88
|
+
changed = False
|
|
89
|
+
if value is not None:
|
|
90
|
+
self._values[index] = value
|
|
91
|
+
changed = True
|
|
92
|
+
|
|
93
|
+
if point is not None:
|
|
94
|
+
self._points[index] = point
|
|
95
|
+
changed = True
|
|
96
|
+
|
|
97
|
+
if label is not None:
|
|
98
|
+
self._labels[index] = label
|
|
99
|
+
changed = True
|
|
100
|
+
|
|
101
|
+
if changed:
|
|
102
|
+
self.dataChanged.emit()
|
|
103
|
+
|
|
104
|
+
def remove_points(self, indices):
|
|
105
|
+
"""
|
|
106
|
+
Removes points at the specified indices.
|
|
107
|
+
"""
|
|
108
|
+
if len(indices) == 0:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Sort indices in descending order to avoid shifting issues if we were popping,
|
|
112
|
+
# but numpy delete handles it. For list, we need to be careful.
|
|
113
|
+
# It's better to create a mask or rebuild the list.
|
|
114
|
+
|
|
115
|
+
mask = np.ones(len(self._points), dtype=bool)
|
|
116
|
+
mask[indices] = False
|
|
117
|
+
|
|
118
|
+
self._points = self._points[mask]
|
|
119
|
+
self._values = self._values[mask]
|
|
120
|
+
self._labels = [self._labels[i] for i in range(len(self._labels)) if mask[i]]
|
|
121
|
+
|
|
122
|
+
self.dataChanged.emit()
|
|
123
|
+
|
|
124
|
+
def clear(self):
|
|
125
|
+
"""
|
|
126
|
+
Removes all data.
|
|
127
|
+
"""
|
|
128
|
+
self._points = np.empty((0, 2), dtype=float)
|
|
129
|
+
self._values = np.empty((0,), dtype=float)
|
|
130
|
+
self._labels = []
|
|
131
|
+
self.dataChanged.emit()
|
|
132
|
+
|
|
133
|
+
def get_closest_point(self, x, y, threshold=None):
|
|
134
|
+
"""
|
|
135
|
+
Finds the index of the closest point to (x, y).
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
x, y: Coordinates.
|
|
139
|
+
threshold: Optional maximum distance. If closest point is further, returns None.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
int: Index of the closest point, or None.
|
|
143
|
+
"""
|
|
144
|
+
if len(self._points) == 0:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
# Calculate squared distances
|
|
148
|
+
diff = self._points - np.array([x, y])
|
|
149
|
+
dist_sq = np.sum(diff**2, axis=1)
|
|
150
|
+
|
|
151
|
+
min_idx = np.argmin(dist_sq)
|
|
152
|
+
min_dist_sq = dist_sq[min_idx]
|
|
153
|
+
|
|
154
|
+
if threshold is not None:
|
|
155
|
+
if min_dist_sq > threshold**2:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
return min_idx
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from PySide6.QtCore import QRectF
|
|
2
|
+
import numpy as np
|
|
3
|
+
from fieldview.layers.layer import Layer
|
|
4
|
+
from fieldview.core.data_container import DataContainer
|
|
5
|
+
|
|
6
|
+
class DataLayer(Layer):
|
|
7
|
+
"""
|
|
8
|
+
Base class for layers that visualize data from a DataContainer.
|
|
9
|
+
Handles data change signals and excluded indices.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, data_container: DataContainer, parent=None):
|
|
12
|
+
super().__init__(parent)
|
|
13
|
+
self._data_container = data_container
|
|
14
|
+
self._excluded_indices = set()
|
|
15
|
+
|
|
16
|
+
# Connect signal
|
|
17
|
+
self._data_container.dataChanged.connect(self.on_data_changed)
|
|
18
|
+
# Initial update
|
|
19
|
+
self.on_data_changed()
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def data_container(self):
|
|
23
|
+
return self._data_container
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def excluded_indices(self):
|
|
27
|
+
return self._excluded_indices
|
|
28
|
+
|
|
29
|
+
def set_excluded_indices(self, indices):
|
|
30
|
+
"""
|
|
31
|
+
Sets the set of indices to exclude from visualization.
|
|
32
|
+
"""
|
|
33
|
+
self._excluded_indices = set(indices)
|
|
34
|
+
self.update_layer()
|
|
35
|
+
|
|
36
|
+
def add_excluded_index(self, index):
|
|
37
|
+
self._excluded_indices.add(index)
|
|
38
|
+
self.update_layer()
|
|
39
|
+
|
|
40
|
+
def remove_excluded_index(self, index):
|
|
41
|
+
if index in self._excluded_indices:
|
|
42
|
+
self._excluded_indices.remove(index)
|
|
43
|
+
self.update_layer()
|
|
44
|
+
|
|
45
|
+
def clear_excluded_indices(self):
|
|
46
|
+
self._excluded_indices.clear()
|
|
47
|
+
self.update_layer()
|
|
48
|
+
|
|
49
|
+
def on_data_changed(self):
|
|
50
|
+
"""
|
|
51
|
+
Slot called when DataContainer data changes.
|
|
52
|
+
"""
|
|
53
|
+
self._update_bounding_rect()
|
|
54
|
+
self.update_layer()
|
|
55
|
+
|
|
56
|
+
def _update_bounding_rect(self):
|
|
57
|
+
points = self._data_container.points
|
|
58
|
+
if len(points) == 0:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
min_x = np.min(points[:, 0])
|
|
62
|
+
max_x = np.max(points[:, 0])
|
|
63
|
+
min_y = np.min(points[:, 1])
|
|
64
|
+
max_y = np.max(points[:, 1])
|
|
65
|
+
|
|
66
|
+
# Add padding (e.g., for icons or text)
|
|
67
|
+
padding = 50
|
|
68
|
+
rect = QRectF(min_x - padding, min_y - padding,
|
|
69
|
+
max_x - min_x + 2*padding, max_y - min_y + 2*padding)
|
|
70
|
+
self.set_bounding_rect(rect)
|
|
71
|
+
|
|
72
|
+
def get_valid_data(self):
|
|
73
|
+
"""
|
|
74
|
+
Returns points, values, and labels excluding the excluded indices.
|
|
75
|
+
"""
|
|
76
|
+
points = self._data_container.points
|
|
77
|
+
values = self._data_container.values
|
|
78
|
+
labels = self._data_container.labels
|
|
79
|
+
|
|
80
|
+
if not self._excluded_indices:
|
|
81
|
+
return points, values, labels
|
|
82
|
+
|
|
83
|
+
# Create a mask for valid indices
|
|
84
|
+
mask = [i for i in range(len(points)) if i not in self._excluded_indices]
|
|
85
|
+
|
|
86
|
+
# Filter labels (list)
|
|
87
|
+
valid_labels = [labels[i] for i in mask]
|
|
88
|
+
|
|
89
|
+
return points[mask], values[mask], valid_labels
|