kaxe 1.8.1.dev0__tar.gz → 1.8.1.dev4__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.
- {kaxe-1.8.1.dev0/src/kaxe.egg-info → kaxe-1.8.1.dev4}/PKG-INFO +41 -4
- kaxe-1.8.1.dev4/README.md +88 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/pyproject.toml +5 -1
- kaxe-1.8.1.dev4/src/kaxe/__main__.py +6 -0
- kaxe-1.8.1.dev4/src/kaxe/_require_viewer.py +8 -0
- kaxe-1.8.1.dev4/src/kaxe/cli.py +8 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/window.py +35 -3
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/d3/plot3d.py +4 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/grid.py +35 -3
- kaxe-1.8.1.dev4/src/kaxe/viewer/__init__.py +11 -0
- kaxe-1.8.1.dev4/src/kaxe/viewer/cli.py +89 -0
- kaxe-1.8.1.dev4/src/kaxe/viewer/client.py +91 -0
- kaxe-1.8.1.dev4/src/kaxe/viewer/server.py +79 -0
- kaxe-1.8.1.dev4/src/kaxe/viewer/window.py +582 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4/src/kaxe.egg-info}/PKG-INFO +41 -4
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe.egg-info/SOURCES.txt +11 -1
- kaxe-1.8.1.dev4/src/kaxe.egg-info/entry_points.txt +2 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe.egg-info/requires.txt +3 -0
- kaxe-1.8.1.dev4/tests/test_viewer.py +295 -0
- kaxe-1.8.1.dev0/README.md +0 -53
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/LICENSE +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/MANIFEST.in +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/setup.cfg +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/_require_3d.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/chart/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/chart/bar.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/chart/box.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/chart/pie.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/chart/qqplot.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/axis.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/bounds.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/color.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/backend.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/camera.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/helper.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/hud.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/color.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/line.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/point.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/pointer.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/objects/triangle.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/openglrender.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/d3/translator.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/draw.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/fileloader.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/helper.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/ipython_display.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/legend.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/line.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/marker.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/palette.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/profiler.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/progress.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/round.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/shapes.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/styles.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/svg.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/svg_pdf.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/symbol.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/text.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/core/thin.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/data/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/data/excel.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/_lazy.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/arrow.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/bubble.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/contour.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/equation.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/fill.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/function.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/guide_grid.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/inequality.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/map.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/parameter.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/pillar.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d2/point.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/base.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/function.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/mesh.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/point.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/d3/potato.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/function.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/legend.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/mapdata.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/point.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/objects/text.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/_lazy.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/box.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/constants.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/d3/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/d3/axes.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/d3/geometry.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/d3/variants.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/double.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/empty.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/log.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/polar.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/standard.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/themes.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/zoom.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/plot/zoom_connector.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/codec.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/context.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/document.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/registry.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/sample.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/sampled_curve.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/serializers.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/project/window.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/__init__.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.bright-oblique.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.bright-roman.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.bright-semibold.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.bright-semiboldoblique.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.classical-serif-italic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.concrete-bold.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.concrete-bolditalic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.concrete-italic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.concrete-roman.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-bold.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-boldoblique.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-demi-condensed-demicondensed.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-medium.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-oblique.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-bold.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-bolditalic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-boldslanted.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-romanslanted.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-italic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-roman.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.serif-upright-italic-uprightitalic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bold.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bolditalic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-italic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-light.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-lightoblique.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-regular.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-italic.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-medium.ttf +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/computer-modern-family/readme.txt +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe/resources/logo-small.png +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe.egg-info/dependency_links.txt +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/src/kaxe.egg-info/top_level.txt +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/tests/test.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/tests/test2.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/tests/test3.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/tests/test_4.py +0 -0
- {kaxe-1.8.1.dev0 → kaxe-1.8.1.dev4}/tests/test_5.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kaxe
|
|
3
|
-
Version: 1.8.1.
|
|
3
|
+
Version: 1.8.1.dev4
|
|
4
4
|
Summary: A small graphing tool for functions, points, equations and more
|
|
5
5
|
Author-email: Valter Yde Daugberg <valteryde@hotmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/valteryde/kaxe
|
|
@@ -27,6 +27,8 @@ Requires-Dist: psutil; extra == "3d"
|
|
|
27
27
|
Provides-Extra: pdf
|
|
28
28
|
Requires-Dist: reportlab>=4.0; extra == "pdf"
|
|
29
29
|
Requires-Dist: fonttools>=4.0; extra == "pdf"
|
|
30
|
+
Provides-Extra: viewer
|
|
31
|
+
Requires-Dist: pyglet>=2.0; extra == "viewer"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|
|
|
@@ -42,9 +44,9 @@ Documentation: [kaxe.readthedocs.io](https://kaxe.readthedocs.io/en/latest/)
|
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
46
|
pip install kaxe # 2D plotting (default)
|
|
45
|
-
pip install kaxe[
|
|
46
|
-
pip install kaxe[
|
|
47
|
-
pip install kaxe[
|
|
47
|
+
pip install kaxe[viewer] # add interactive desktop viewer (pyglet)
|
|
48
|
+
pip install kaxe[3d] # add 3D plotting (OpenGL)
|
|
49
|
+
pip install kaxe[pdf] # add PDF export
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
## Quick example
|
|
@@ -57,10 +59,45 @@ plt.add(kaxe.Function2D(lambda x: x**2 - 4))
|
|
|
57
59
|
plt.add(kaxe.Points2D([1, 2, 3], [1, 4, 9]))
|
|
58
60
|
plt.theme(kaxe.Themes.A4Medium)
|
|
59
61
|
plt.title("$x^2 - 4$")
|
|
62
|
+
plt.show() # streams to viewer if open, or opens system viewer
|
|
60
63
|
plt.save("figure.png")
|
|
61
64
|
plt.save("figure.svg")
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
## Interactive Plot Viewer (Infinite Canvas)
|
|
68
|
+
|
|
69
|
+
Kaxe includes a dedicated, lightweight OpenGL workspace powered by [pyglet](https://pyglet.org) so you can arrange, inspect, and compare multiple plots on an infinite canvas without cluttering your desktop with preview windows.
|
|
70
|
+
|
|
71
|
+
1. **Start the viewer** in a terminal:
|
|
72
|
+
```bash
|
|
73
|
+
kaxe viewer
|
|
74
|
+
```
|
|
75
|
+
2. **Tag and show your plots**:
|
|
76
|
+
```python
|
|
77
|
+
# Tag plots with an ID to manage them on the board:
|
|
78
|
+
plt1 = kaxe.Plot().id("loss").title("Training Loss")
|
|
79
|
+
plt1.show()
|
|
80
|
+
|
|
81
|
+
plt2 = kaxe.Plot().id("accuracy").title("Validation Accuracy")
|
|
82
|
+
plt2.show()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
When the viewer is open, `.show()` instantly streams plots to cards on your canvas. If you re-run code with the same ID, the card updates in-place and preserves previous runs as versions (`v1`, `v2`, etc.). If the viewer is closed, `.show()` automatically falls back to your operating system's default viewer.
|
|
86
|
+
|
|
87
|
+
**Canvas & Card Controls:**
|
|
88
|
+
* **Click & Drag on a Card**: Move that card freely across the canvas
|
|
89
|
+
* **Click & Drag on Empty Space**: Pan the camera across the workspace
|
|
90
|
+
* **Mouse Scroll**: Smooth zoom anchored to cursor
|
|
91
|
+
* **`[A]`**: Auto-arrange all cards in a neat row
|
|
92
|
+
* **`[Space]` or `[R]`**: Fit all cards on the canvas into view
|
|
93
|
+
* **`[1]`**: View canvas at 100% native pixel scale
|
|
94
|
+
* **`[←]` / `[→]`**: Cycle through versions (`v1`, `v2`, ...) of the selected card
|
|
95
|
+
* **`[Backspace]` / `[Delete]`**: Remove the selected card from the board
|
|
96
|
+
* **`[C]`** *(or Cmd/Ctrl+C)*: Copy selected plot to system clipboard
|
|
97
|
+
* **`[S]`** *(or Cmd/Ctrl+S)*: Save selected plot to timestamped PNG
|
|
98
|
+
* **`[H]`**: Toggle status & shortcuts HUD
|
|
99
|
+
|
|
100
|
+
|
|
64
101
|
See the [getting started guide](https://kaxe.readthedocs.io/en/latest/pages/start.html), [recipes](https://kaxe.readthedocs.io/en/latest/pages/recipes.html), [styling guide](https://kaxe.readthedocs.io/en/latest/pages/styling.html), and [export guide](https://kaxe.readthedocs.io/en/latest/pages/export.html) for the full API.
|
|
65
102
|
|
|
66
103
|
## Goals
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
# Kaxe
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Kaxe is a lightweight, pure-Python plotting library for publication-quality figures in LaTeX documents. Create plots and charts with a simple object-oriented API, style them for A4 pages, and export to PNG or SVG.
|
|
7
|
+
|
|
8
|
+
Documentation: [kaxe.readthedocs.io](https://kaxe.readthedocs.io/en/latest/)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install kaxe # 2D plotting (default)
|
|
14
|
+
pip install kaxe[viewer] # add interactive desktop viewer (pyglet)
|
|
15
|
+
pip install kaxe[3d] # add 3D plotting (OpenGL)
|
|
16
|
+
pip install kaxe[pdf] # add PDF export
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick example
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import kaxe
|
|
23
|
+
|
|
24
|
+
plt = kaxe.Plot([-5, 5, -5, 5])
|
|
25
|
+
plt.add(kaxe.Function2D(lambda x: x**2 - 4))
|
|
26
|
+
plt.add(kaxe.Points2D([1, 2, 3], [1, 4, 9]))
|
|
27
|
+
plt.theme(kaxe.Themes.A4Medium)
|
|
28
|
+
plt.title("$x^2 - 4$")
|
|
29
|
+
plt.show() # streams to viewer if open, or opens system viewer
|
|
30
|
+
plt.save("figure.png")
|
|
31
|
+
plt.save("figure.svg")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Interactive Plot Viewer (Infinite Canvas)
|
|
35
|
+
|
|
36
|
+
Kaxe includes a dedicated, lightweight OpenGL workspace powered by [pyglet](https://pyglet.org) so you can arrange, inspect, and compare multiple plots on an infinite canvas without cluttering your desktop with preview windows.
|
|
37
|
+
|
|
38
|
+
1. **Start the viewer** in a terminal:
|
|
39
|
+
```bash
|
|
40
|
+
kaxe viewer
|
|
41
|
+
```
|
|
42
|
+
2. **Tag and show your plots**:
|
|
43
|
+
```python
|
|
44
|
+
# Tag plots with an ID to manage them on the board:
|
|
45
|
+
plt1 = kaxe.Plot().id("loss").title("Training Loss")
|
|
46
|
+
plt1.show()
|
|
47
|
+
|
|
48
|
+
plt2 = kaxe.Plot().id("accuracy").title("Validation Accuracy")
|
|
49
|
+
plt2.show()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When the viewer is open, `.show()` instantly streams plots to cards on your canvas. If you re-run code with the same ID, the card updates in-place and preserves previous runs as versions (`v1`, `v2`, etc.). If the viewer is closed, `.show()` automatically falls back to your operating system's default viewer.
|
|
53
|
+
|
|
54
|
+
**Canvas & Card Controls:**
|
|
55
|
+
* **Click & Drag on a Card**: Move that card freely across the canvas
|
|
56
|
+
* **Click & Drag on Empty Space**: Pan the camera across the workspace
|
|
57
|
+
* **Mouse Scroll**: Smooth zoom anchored to cursor
|
|
58
|
+
* **`[A]`**: Auto-arrange all cards in a neat row
|
|
59
|
+
* **`[Space]` or `[R]`**: Fit all cards on the canvas into view
|
|
60
|
+
* **`[1]`**: View canvas at 100% native pixel scale
|
|
61
|
+
* **`[←]` / `[→]`**: Cycle through versions (`v1`, `v2`, ...) of the selected card
|
|
62
|
+
* **`[Backspace]` / `[Delete]`**: Remove the selected card from the board
|
|
63
|
+
* **`[C]`** *(or Cmd/Ctrl+C)*: Copy selected plot to system clipboard
|
|
64
|
+
* **`[S]`** *(or Cmd/Ctrl+S)*: Save selected plot to timestamped PNG
|
|
65
|
+
* **`[H]`**: Toggle status & shortcuts HUD
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
See the [getting started guide](https://kaxe.readthedocs.io/en/latest/pages/start.html), [recipes](https://kaxe.readthedocs.io/en/latest/pages/recipes.html), [styling guide](https://kaxe.readthedocs.io/en/latest/pages/styling.html), and [export guide](https://kaxe.readthedocs.io/en/latest/pages/export.html) for the full API.
|
|
69
|
+
|
|
70
|
+
## Goals
|
|
71
|
+
|
|
72
|
+
Kaxe was made to create simple, aesthetic graphs for articles, reports, and other academic work in LaTeX:
|
|
73
|
+
|
|
74
|
+
* Plots that fit the look of LaTeX documents (Computer Modern math via [fondi](https://github.com/valteryde/fondi))
|
|
75
|
+
* A straightforward object-oriented interface: create a window, add objects, style, save
|
|
76
|
+
|
|
77
|
+
## Examples of plots
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+

|
|
81
|
+

|
|
82
|
+

|
|
83
|
+

|
|
84
|
+

|
|
85
|
+

|
|
86
|
+

|
|
87
|
+

|
|
88
|
+

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kaxe"
|
|
3
|
-
version = "1.8.1.
|
|
3
|
+
version = "1.8.1.dev4"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Valter Yde Daugberg", email="valteryde@hotmail.com" },
|
|
6
6
|
]
|
|
@@ -31,6 +31,10 @@ core = []
|
|
|
31
31
|
"psutil",
|
|
32
32
|
]
|
|
33
33
|
pdf = ["reportlab>=4.0", "fonttools>=4.0"]
|
|
34
|
+
viewer = ["pyglet>=2.0"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
kaxe = "kaxe.cli:main"
|
|
34
38
|
|
|
35
39
|
[project.urls]
|
|
36
40
|
Homepage = "https://github.com/valteryde/kaxe"
|
|
@@ -11,6 +11,7 @@ from .svg import SvgDocument, infer_format, is_file_path
|
|
|
11
11
|
from .progress import make_progress_bar
|
|
12
12
|
from .ipython_display import display_png_bytes, is_notebook, to_png_bytes
|
|
13
13
|
from .styles import assign_series_color, _palette_color_at
|
|
14
|
+
from ..viewer.client import try_show_in_viewer
|
|
14
15
|
from PIL import Image
|
|
15
16
|
|
|
16
17
|
|
|
@@ -646,23 +647,54 @@ class Window(AttrObject):
|
|
|
646
647
|
logging.info('Total time to save {}s'.format(str(round(time.time() - totStartTime, 4))))
|
|
647
648
|
|
|
648
649
|
|
|
649
|
-
def
|
|
650
|
+
def id(self, plot_id: str):
|
|
651
|
+
"""Assign a persistent identifier to this plot for the Kaxe Viewer."""
|
|
652
|
+
self.plot_id = str(plot_id)
|
|
653
|
+
return self
|
|
654
|
+
|
|
655
|
+
def show(self, id: Optional[str] = None):
|
|
650
656
|
"""
|
|
651
|
-
Show the current image using `Pillow.Image.Show`
|
|
657
|
+
Show the current image using `Pillow.Image.Show` or the Kaxe Viewer.
|
|
652
658
|
|
|
653
659
|
If a cached image is available, it will be used to save the file.
|
|
654
660
|
Otherwise, the image will be generated and then saved.
|
|
655
661
|
|
|
662
|
+
Parameters
|
|
663
|
+
----------
|
|
664
|
+
id : str, optional
|
|
665
|
+
Identifier for this plot when sent to the Kaxe Viewer canvas.
|
|
666
|
+
|
|
656
667
|
Examples
|
|
657
668
|
--------
|
|
658
|
-
>>> plt.show(
|
|
669
|
+
>>> plt.show()
|
|
670
|
+
>>> plt.show(id="my_graph_1")
|
|
659
671
|
"""
|
|
672
|
+
if id is not None:
|
|
673
|
+
self.plot_id = str(id)
|
|
674
|
+
|
|
660
675
|
data = to_png_bytes(self)
|
|
661
676
|
if is_notebook():
|
|
662
677
|
display_png_bytes(data)
|
|
678
|
+
elif try_show_in_viewer(
|
|
679
|
+
data,
|
|
680
|
+
title=getattr(self, "titleText", ""),
|
|
681
|
+
plot_id=getattr(self, "plot_id", None),
|
|
682
|
+
):
|
|
683
|
+
return
|
|
663
684
|
else:
|
|
664
685
|
Image.open(BytesIO(data)).show()
|
|
665
686
|
|
|
687
|
+
def open(self, id: Optional[str] = None):
|
|
688
|
+
"""
|
|
689
|
+
Show the current image or stream to viewer (alias for show()).
|
|
690
|
+
|
|
691
|
+
Examples
|
|
692
|
+
--------
|
|
693
|
+
>>> plt.open()
|
|
694
|
+
>>> plt.open(id="my_graph_1")
|
|
695
|
+
"""
|
|
696
|
+
return self.show(id=id)
|
|
697
|
+
|
|
666
698
|
def _repr_png_(self):
|
|
667
699
|
if not is_notebook():
|
|
668
700
|
return None
|
|
@@ -547,6 +547,10 @@ class Plot3D(Plot3DAxesMixin, Window):
|
|
|
547
547
|
else:
|
|
548
548
|
self.save(None).show()
|
|
549
549
|
|
|
550
|
+
def open(self, gui=True):
|
|
551
|
+
"""Show the current plot (alias for show())."""
|
|
552
|
+
return self.show(gui=gui)
|
|
553
|
+
|
|
550
554
|
def title(self, firstAxis=None, secondAxis=None, thirdAxis=None):
|
|
551
555
|
"""
|
|
552
556
|
Adds title to the plot.
|
|
@@ -4,6 +4,7 @@ from typing import Optional, Union
|
|
|
4
4
|
from ..core.styles import AttrObject, AttrMap
|
|
5
5
|
from ..core.window import *
|
|
6
6
|
from ..core.ipython_display import display_png_bytes, is_notebook, to_png_bytes
|
|
7
|
+
from ..viewer.client import try_show_in_viewer
|
|
7
8
|
from ..core.legend import LegendBox
|
|
8
9
|
from ..core.svg import (
|
|
9
10
|
SvgDocument,
|
|
@@ -489,23 +490,54 @@ class Grid(AttrObject):
|
|
|
489
490
|
img.save(fname, format="png")
|
|
490
491
|
|
|
491
492
|
|
|
492
|
-
def
|
|
493
|
+
def id(self, plot_id: str):
|
|
494
|
+
"""Assign a persistent identifier to this grid for the Kaxe Viewer."""
|
|
495
|
+
self.plot_id = str(plot_id)
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
def show(self, id: Optional[str] = None):
|
|
493
499
|
"""
|
|
494
|
-
Show the current image using `Pillow.Image.Show`
|
|
500
|
+
Show the current image using `Pillow.Image.Show` or the Kaxe Viewer.
|
|
495
501
|
|
|
496
502
|
If a cached image is available, it will be used to save the file.
|
|
497
503
|
Otherwise, the image will be generated and then saved.
|
|
498
504
|
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
id : str, optional
|
|
508
|
+
Identifier for this grid when sent to the Kaxe Viewer canvas.
|
|
509
|
+
|
|
499
510
|
Examples
|
|
500
511
|
--------
|
|
501
|
-
>>>
|
|
512
|
+
>>> grid.show()
|
|
513
|
+
>>> grid.show(id="my_grid_1")
|
|
502
514
|
"""
|
|
515
|
+
if id is not None:
|
|
516
|
+
self.plot_id = str(id)
|
|
517
|
+
|
|
503
518
|
data = to_png_bytes(self)
|
|
504
519
|
if is_notebook():
|
|
505
520
|
display_png_bytes(data)
|
|
521
|
+
elif try_show_in_viewer(
|
|
522
|
+
data,
|
|
523
|
+
title="Grid",
|
|
524
|
+
plot_id=getattr(self, "plot_id", None),
|
|
525
|
+
):
|
|
526
|
+
return
|
|
506
527
|
else:
|
|
507
528
|
Image.open(BytesIO(data)).show()
|
|
508
529
|
|
|
530
|
+
def open(self, id: Optional[str] = None):
|
|
531
|
+
"""
|
|
532
|
+
Show the current grid or stream to viewer (alias for show()).
|
|
533
|
+
|
|
534
|
+
Examples
|
|
535
|
+
--------
|
|
536
|
+
>>> grid.open()
|
|
537
|
+
>>> grid.open(id="my_grid_1")
|
|
538
|
+
"""
|
|
539
|
+
return self.show(id=id)
|
|
540
|
+
|
|
509
541
|
def _repr_png_(self):
|
|
510
542
|
if not is_notebook():
|
|
511
543
|
return None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Kaxe Viewer package."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_PORT, get_viewer_port, is_viewer_active, send_to_viewer, try_show_in_viewer
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DEFAULT_PORT",
|
|
7
|
+
"get_viewer_port",
|
|
8
|
+
"is_viewer_active",
|
|
9
|
+
"send_to_viewer",
|
|
10
|
+
"try_show_in_viewer",
|
|
11
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Command line interface for launching Kaxe Viewer."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import queue
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .client import get_viewer_port, is_viewer_active
|
|
8
|
+
from .server import start_server_thread
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_viewer(port: int = 41783):
|
|
12
|
+
"""Launch the Kaxe Viewer Pyglet window and HTTP server."""
|
|
13
|
+
# Ensure optional dependency is installed
|
|
14
|
+
from .._require_viewer import require_viewer
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
require_viewer()
|
|
18
|
+
except ImportError as e:
|
|
19
|
+
print(f"\n[Kaxe] {e}\n", file=sys.stderr)
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
import pyglet
|
|
23
|
+
from .window import KaxeViewerWindow
|
|
24
|
+
|
|
25
|
+
# Check if a viewer is already running on this port
|
|
26
|
+
if is_viewer_active(port):
|
|
27
|
+
print(f"\n[Kaxe Viewer] Port {port} is already in use.", file=sys.stderr)
|
|
28
|
+
print("A Kaxe Viewer instance is likely already open.\n", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
plot_queue = queue.Queue()
|
|
32
|
+
|
|
33
|
+
# Start HTTP server on background thread
|
|
34
|
+
server, thread = start_server_thread(port, plot_queue)
|
|
35
|
+
|
|
36
|
+
print("=" * 64)
|
|
37
|
+
print(f" Kaxe Viewer running on http://127.0.0.1:{port}")
|
|
38
|
+
print(" Keep this terminal open while working.")
|
|
39
|
+
print(" In another terminal, run your scripts calling .show()!")
|
|
40
|
+
print()
|
|
41
|
+
print(" Controls:")
|
|
42
|
+
print(" • Mouse Wheel : Zoom (centered at cursor)")
|
|
43
|
+
print(" • Left Click Drag : Pan image")
|
|
44
|
+
print(" • [R] or [Space] : Fit plot to window")
|
|
45
|
+
print(" • [1] : 100% native resolution")
|
|
46
|
+
print(" • [←] / [→] : Cycle through previous plots")
|
|
47
|
+
print(" • [C] : Copy image to clipboard")
|
|
48
|
+
print(" • [S] : Save image to file")
|
|
49
|
+
print(" • [H] : Toggle HUD overlay")
|
|
50
|
+
print("=" * 64)
|
|
51
|
+
|
|
52
|
+
# Create Pyglet window and run Cocoa / OpenGL event loop on main thread
|
|
53
|
+
window = KaxeViewerWindow(plot_queue)
|
|
54
|
+
pyglet.app.run()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv=None):
|
|
58
|
+
"""Entrypoint for the `kaxe` CLI command."""
|
|
59
|
+
if argv is None:
|
|
60
|
+
argv = sys.argv[1:]
|
|
61
|
+
|
|
62
|
+
parser = argparse.ArgumentParser(
|
|
63
|
+
prog="kaxe",
|
|
64
|
+
description="Kaxe graphing & plotting toolkit CLI",
|
|
65
|
+
)
|
|
66
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
|
|
67
|
+
|
|
68
|
+
# `kaxe viewer` command
|
|
69
|
+
viewer_parser = subparsers.add_parser(
|
|
70
|
+
"viewer",
|
|
71
|
+
help="Start the standalone Kaxe interactive plot viewer",
|
|
72
|
+
)
|
|
73
|
+
viewer_parser.add_argument(
|
|
74
|
+
"--port",
|
|
75
|
+
type=int,
|
|
76
|
+
default=get_viewer_port(),
|
|
77
|
+
help=f"Port to listen on (default: {get_viewer_port()})",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args(argv)
|
|
81
|
+
|
|
82
|
+
if args.subcommand == "viewer":
|
|
83
|
+
run_viewer(port=args.port)
|
|
84
|
+
else:
|
|
85
|
+
parser.print_help()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Client-side probe and transmitter for Kaxe Viewer.
|
|
2
|
+
|
|
3
|
+
This module intentionally uses ONLY Python standard library modules
|
|
4
|
+
(socket, urllib, os) so that calling `plt.show()` never requires
|
|
5
|
+
pyglet or any other viewer dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
DEFAULT_PORT = 41783
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_viewer_port() -> int:
|
|
19
|
+
"""Return the configured viewer port from environment or the default."""
|
|
20
|
+
env_port = os.getenv("KAXE_VIEWER_PORT")
|
|
21
|
+
if env_port:
|
|
22
|
+
try:
|
|
23
|
+
return int(env_port)
|
|
24
|
+
except ValueError:
|
|
25
|
+
pass
|
|
26
|
+
return DEFAULT_PORT
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_viewer_active(port: Optional[int] = None, timeout: float = 0.03) -> bool:
|
|
30
|
+
"""Check if the Kaxe Viewer is currently listening on localhost.
|
|
31
|
+
|
|
32
|
+
Uses a fast socket connection test with a short timeout (~30ms).
|
|
33
|
+
When no viewer is running, the OS kernel rejects the connection
|
|
34
|
+
virtually instantaneously (<1ms).
|
|
35
|
+
"""
|
|
36
|
+
if port is None:
|
|
37
|
+
port = get_viewer_port()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with socket.create_connection(("127.0.0.1", port), timeout=timeout):
|
|
41
|
+
return True
|
|
42
|
+
except (OSError, ConnectionRefusedError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def send_to_viewer(
|
|
47
|
+
data: bytes,
|
|
48
|
+
title: str = "",
|
|
49
|
+
plot_id: Optional[str] = None,
|
|
50
|
+
port: Optional[int] = None,
|
|
51
|
+
timeout: float = 2.0,
|
|
52
|
+
) -> bool:
|
|
53
|
+
"""Send image PNG bytes to the active Kaxe Viewer over HTTP POST.
|
|
54
|
+
|
|
55
|
+
Returns True if successfully received, False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
if port is None:
|
|
58
|
+
port = get_viewer_port()
|
|
59
|
+
|
|
60
|
+
url = f"http://127.0.0.1:{port}/show"
|
|
61
|
+
safe_title = urllib.parse.quote(title or "")
|
|
62
|
+
safe_id = urllib.parse.quote(plot_id or "")
|
|
63
|
+
headers = {
|
|
64
|
+
"Content-Type": "image/png",
|
|
65
|
+
"X-Kaxe-Title": safe_title,
|
|
66
|
+
"X-Kaxe-Id": safe_id,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
72
|
+
return response.status == 200
|
|
73
|
+
except (urllib.error.URLError, OSError):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def try_show_in_viewer(
|
|
78
|
+
data: bytes,
|
|
79
|
+
title: str = "",
|
|
80
|
+
plot_id: Optional[str] = None,
|
|
81
|
+
) -> bool:
|
|
82
|
+
"""Convenience helper: probe port and send image if viewer is alive.
|
|
83
|
+
|
|
84
|
+
Returns True if sent to viewer, False if fallback is needed.
|
|
85
|
+
"""
|
|
86
|
+
port = get_viewer_port()
|
|
87
|
+
if is_viewer_active(port):
|
|
88
|
+
if send_to_viewer(data, title=title, plot_id=plot_id, port=port):
|
|
89
|
+
print("[Kaxe] Plot streamed to viewer", flush=True)
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Lightweight HTTP server embedded inside Kaxe Viewer.
|
|
2
|
+
|
|
3
|
+
Uses Python standard library `http.server.ThreadingHTTPServer` to receive
|
|
4
|
+
plot images and metadata from client scripts without external web frameworks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
import queue
|
|
9
|
+
import threading
|
|
10
|
+
import urllib.parse
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlotRequestHandler(BaseHTTPRequestHandler):
|
|
15
|
+
"""Handles incoming /show POST requests and /ping health checks."""
|
|
16
|
+
|
|
17
|
+
# Reference to the shared Queue injected by start_server
|
|
18
|
+
plot_queue: queue.Queue = queue.Queue()
|
|
19
|
+
|
|
20
|
+
def do_GET(self):
|
|
21
|
+
if self.path in ("/ping", "/health"):
|
|
22
|
+
self.send_response(200)
|
|
23
|
+
self.send_header("Content-Type", "text/plain")
|
|
24
|
+
self.end_headers()
|
|
25
|
+
self.wfile.write(b"OK")
|
|
26
|
+
else:
|
|
27
|
+
self.send_response(404)
|
|
28
|
+
self.end_headers()
|
|
29
|
+
|
|
30
|
+
def do_POST(self):
|
|
31
|
+
if self.path == "/show":
|
|
32
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
33
|
+
if content_length <= 0:
|
|
34
|
+
self.send_response(400)
|
|
35
|
+
self.end_headers()
|
|
36
|
+
self.wfile.write(b"Empty body")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
img_bytes = self.rfile.read(content_length)
|
|
40
|
+
raw_title = self.headers.get("X-Kaxe-Title", "")
|
|
41
|
+
title = urllib.parse.unquote(raw_title) if raw_title else ""
|
|
42
|
+
|
|
43
|
+
raw_id = self.headers.get("X-Kaxe-Id", "")
|
|
44
|
+
plot_id = urllib.parse.unquote(raw_id) if raw_id else ""
|
|
45
|
+
|
|
46
|
+
item: Dict[str, Any] = {
|
|
47
|
+
"data": img_bytes,
|
|
48
|
+
"title": title,
|
|
49
|
+
"plot_id": plot_id,
|
|
50
|
+
}
|
|
51
|
+
self.plot_queue.put(item)
|
|
52
|
+
|
|
53
|
+
self.send_response(200)
|
|
54
|
+
self.send_header("Content-Type", "text/plain")
|
|
55
|
+
self.end_headers()
|
|
56
|
+
self.wfile.write(b"OK")
|
|
57
|
+
else:
|
|
58
|
+
self.send_response(404)
|
|
59
|
+
self.end_headers()
|
|
60
|
+
|
|
61
|
+
def log_message(self, format: str, *args):
|
|
62
|
+
# Format messages cleanly in terminal
|
|
63
|
+
first_arg = args[0] if args else ""
|
|
64
|
+
if "POST /show" in str(first_arg):
|
|
65
|
+
print(f"[Kaxe Viewer] New plot received ({format % args})")
|
|
66
|
+
# Suppress noisy health checks in console
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def start_server_thread(
|
|
70
|
+
port: int,
|
|
71
|
+
plot_queue: queue.Queue,
|
|
72
|
+
host: str = "127.0.0.1",
|
|
73
|
+
) -> tuple[ThreadingHTTPServer, threading.Thread]:
|
|
74
|
+
"""Start the HTTP server on a daemon background thread."""
|
|
75
|
+
PlotRequestHandler.plot_queue = plot_queue
|
|
76
|
+
server = ThreadingHTTPServer((host, port), PlotRequestHandler)
|
|
77
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
78
|
+
thread.start()
|
|
79
|
+
return server, thread
|