fastplotpy 0.3.0a1__py3-none-win_amd64.whl
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.
- fastplotpy/README.md +70 -0
- fastplotpy/__init__.py +33 -0
- fastplotpy/assets/dist/assets/scene.worker-CcnexaKr.js +1 -0
- fastplotpy/assets/dist/index.js +15071 -0
- fastplotpy/assets/native/NotoSans-OFL.txt +96 -0
- fastplotpy/assets/native/THIRD_PARTY_NOTICES.md +24 -0
- fastplotpy/assets/native/fastplot_python_abi.dll +0 -0
- fastplotpy/assets/package.json +1 -0
- fastplotpy/axes_validation.py +102 -0
- fastplotpy/camera3d.py +104 -0
- fastplotpy/color_scale.py +88 -0
- fastplotpy/defaults/area-fill.json +5 -0
- fastplotpy/defaults/bar.json +22 -0
- fastplotpy/defaults/camera3d.json +22 -0
- fastplotpy/defaults/cartesian-references.json +10 -0
- fastplotpy/defaults/control.json +3 -0
- fastplotpy/defaults/crosshair.json +19 -0
- fastplotpy/defaults/error-bars.json +7 -0
- fastplotpy/defaults/heatmap.json +70 -0
- fastplotpy/defaults/interaction.json +3 -0
- fastplotpy/defaults/layout.json +32 -0
- fastplotpy/defaults/legend.json +24 -0
- fastplotpy/defaults/line-axes.json +28 -0
- fastplotpy/defaults/line3d.json +13 -0
- fastplotpy/defaults/markers.json +6 -0
- fastplotpy/defaults/mesh3d.json +13 -0
- fastplotpy/defaults/pan.json +7 -0
- fastplotpy/defaults/pie.json +27 -0
- fastplotpy/defaults/pie3d.json +7 -0
- fastplotpy/defaults/point-labels.json +13 -0
- fastplotpy/defaults/pointClick.json +6 -0
- fastplotpy/defaults/pointSelection.json +13 -0
- fastplotpy/defaults/polar.json +71 -0
- fastplotpy/defaults/polarRotate.json +5 -0
- fastplotpy/defaults/scatter3d.json +39 -0
- fastplotpy/defaults/scene3d.json +61 -0
- fastplotpy/defaults/surface.json +35 -0
- fastplotpy/defaults/tooltip.json +18 -0
- fastplotpy/defaults/trace-colors.json +10 -0
- fastplotpy/defaults/trace-options.json +24 -0
- fastplotpy/defaults/wonderplot.json +58 -0
- fastplotpy/defaults/zoom.json +12 -0
- fastplotpy/defaults.py +15 -0
- fastplotpy/error_bars.py +118 -0
- fastplotpy/exports.py +153 -0
- fastplotpy/families2d.py +896 -0
- fastplotpy/heatmap_labels.py +146 -0
- fastplotpy/interactions.py +243 -0
- fastplotpy/interactive.py +1922 -0
- fastplotpy/js_serialize.py +47 -0
- fastplotpy/lighting3d.py +113 -0
- fastplotpy/line3d_lod.py +138 -0
- fastplotpy/lod3d.py +105 -0
- fastplotpy/mesh3d_lod.py +107 -0
- fastplotpy/native.py +163 -0
- fastplotpy/overlays.py +561 -0
- fastplotpy/point_labels.py +86 -0
- fastplotpy/polar.py +445 -0
- fastplotpy/polygon_clip3d.py +148 -0
- fastplotpy/references.py +72 -0
- fastplotpy/rendering.py +65 -0
- fastplotpy/rust_scenes.py +314 -0
- fastplotpy/scene.py +1185 -0
- fastplotpy/scene3d.py +1215 -0
- fastplotpy/scene3d_accessibility.py +183 -0
- fastplotpy/scene3d_collection.py +158 -0
- fastplotpy/subplots.py +115 -0
- fastplotpy/surface_contours.py +122 -0
- fastplotpy/surface_lod.py +109 -0
- fastplotpy/svg3d.py +235 -0
- fastplotpy/trendline.py +236 -0
- fastplotpy/triangle_wireframe.py +51 -0
- fastplotpy/types.py +3027 -0
- fastplotpy-0.3.0a1.dist-info/METADATA +86 -0
- fastplotpy-0.3.0a1.dist-info/RECORD +78 -0
- fastplotpy-0.3.0a1.dist-info/WHEEL +5 -0
- fastplotpy-0.3.0a1.dist-info/licenses/LICENSE +21 -0
- fastplotpy-0.3.0a1.dist-info/top_level.txt +1 -0
fastplotpy/exports.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Scene export behavior corresponding to fastPlotJs renderers/png.ts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from base64 import b64decode
|
|
7
|
+
from math import isfinite
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from .defaults import get_defaults
|
|
11
|
+
from .scene import plot
|
|
12
|
+
from .native import rustCall, hasRustPlotEngine
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _nonnegative(value: Any, fallback: float) -> float:
|
|
16
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) and isfinite(value) and value >= 0 else fallback
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _title_height(options: Mapping) -> float:
|
|
20
|
+
title = options.get("title")
|
|
21
|
+
subtitle = options.get("subtitle")
|
|
22
|
+
has_title = isinstance(title, str) and bool(title.strip())
|
|
23
|
+
has_subtitle = isinstance(subtitle, str) and bool(subtitle.strip())
|
|
24
|
+
if not (has_title or has_subtitle):
|
|
25
|
+
return 0
|
|
26
|
+
defaults = get_defaults("layout")["decorations"]
|
|
27
|
+
style = options.get("style") or {}
|
|
28
|
+
|
|
29
|
+
def line_height(kind: str) -> float:
|
|
30
|
+
fallback = defaults[kind + "LineHeight"]
|
|
31
|
+
for source in (options.get(kind + "Style"), style.get(kind), style.get("text")):
|
|
32
|
+
if isinstance(source, Mapping) and "lineHeight" in source:
|
|
33
|
+
return _nonnegative(source["lineHeight"], fallback)
|
|
34
|
+
return fallback
|
|
35
|
+
|
|
36
|
+
height = defaults["topPadding"] + defaults["bottomGap"]
|
|
37
|
+
if has_title:
|
|
38
|
+
height += line_height("title")
|
|
39
|
+
if has_subtitle:
|
|
40
|
+
height += line_height("subtitle")
|
|
41
|
+
if has_title:
|
|
42
|
+
height += defaults["subtitleGap"]
|
|
43
|
+
return height
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _export_bounds(options: Mapping) -> dict:
|
|
47
|
+
width, height = options["width"], options["height"]
|
|
48
|
+
margin_defaults = get_defaults("layout")["margin"]
|
|
49
|
+
margin_input = options.get("margin") or {}
|
|
50
|
+
margin = {side: _nonnegative(margin_input.get(key), margin_defaults[key]) for side, key in
|
|
51
|
+
(("left", "l"), ("right", "r"), ("top", "t"), ("bottom", "b"))}
|
|
52
|
+
axes = options.get("axes") or {}
|
|
53
|
+
axis_defaults = get_defaults("line-axes")["plotBounds"]
|
|
54
|
+
left, top = margin["left"], margin["top"] + _title_height(options)
|
|
55
|
+
right, bottom = margin["right"], margin["bottom"]
|
|
56
|
+
if axes.get("show") is True:
|
|
57
|
+
sides = get_defaults("layout")["axisSide"]
|
|
58
|
+
configured_x = {"x": {}}
|
|
59
|
+
configured_x.update(axes.get("xAxes") or {})
|
|
60
|
+
configured_y = {"y": {}}
|
|
61
|
+
configured_y.update(axes.get("yAxes") or {})
|
|
62
|
+
has_left = any(axis.get("show") is not False and axis.get("side", sides["y"]) == "left" for axis in configured_y.values())
|
|
63
|
+
has_right = any(axis.get("show") is not False and axis.get("side", sides["y"]) == "right" for axis in configured_y.values())
|
|
64
|
+
has_top = any(axis.get("show") is not False and axis.get("side", sides["x"]) == "top" for axis in configured_x.values())
|
|
65
|
+
has_bottom = any(axis.get("show") is not False and axis.get("side", sides["x"]) == "bottom" for axis in configured_x.values())
|
|
66
|
+
left += axis_defaults["left"] if has_left else axis_defaults["right"] if has_right else 0
|
|
67
|
+
right += axis_defaults["left"] if has_right else axis_defaults["right"] if has_left else 0
|
|
68
|
+
top += axis_defaults["bottom"] if has_top else axis_defaults["top"] if has_bottom else 0
|
|
69
|
+
bottom += axis_defaults["bottom"] if has_bottom else axis_defaults["top"] if has_top else 0
|
|
70
|
+
return {"left": left, "top": top, "right": max(left + 1, width - right), "bottom": max(top + 1, height - bottom)}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def buildExportScene(options: Mapping[str, Any]) -> dict:
|
|
74
|
+
bounds = _export_bounds(options)
|
|
75
|
+
local = dict(options)
|
|
76
|
+
local["width"] = max(1, bounds["right"] - bounds["left"])
|
|
77
|
+
local["height"] = max(1, bounds["bottom"] - bounds["top"])
|
|
78
|
+
scene = deepcopy(plot(local))
|
|
79
|
+
scene["width"], scene["height"] = options["width"], options["height"]
|
|
80
|
+
for metadata in scene.get("scenes3d", []):
|
|
81
|
+
metadata["bounds"] = _translate_bounds(metadata["bounds"], bounds)
|
|
82
|
+
if scene.get("scene3d"):
|
|
83
|
+
scene["scene3d"]["bounds"] = _translate_bounds(scene["scene3d"]["bounds"], bounds)
|
|
84
|
+
for primitive in scene.get("primitives", []):
|
|
85
|
+
if primitive.get("bounds"):
|
|
86
|
+
primitive["bounds"] = _translate_bounds(primitive["bounds"], bounds)
|
|
87
|
+
return scene
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _translate_bounds(bounds: Mapping, offset: Mapping) -> dict:
|
|
91
|
+
return {key: value + offset["left" if key in ("left", "right") else "top"] for key, value in bounds.items()}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def renderToPNG(options: Mapping[str, Any]) -> bytes:
|
|
95
|
+
"""Return source-compatible UTF-8 scene JSON bytes (the source API's name is historical)."""
|
|
96
|
+
from .js_serialize import js_json_stringify
|
|
97
|
+
return js_json_stringify(buildExportScene(options)).encode("utf-8")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Scene3dNodePngExportError(RuntimeError):
|
|
101
|
+
"""Source-compatible typed failure for the optional native 3D PNG path."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, code: str, message: str, cause: Exception | None = None):
|
|
104
|
+
super().__init__(message)
|
|
105
|
+
self.code = code
|
|
106
|
+
self.cause = cause
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validate_png(data: bytes, width: int, height: int) -> None:
|
|
110
|
+
valid = len(data) >= 45 and data[:8] == b"\x89PNG\r\n\x1a\n"
|
|
111
|
+
offset, chunk_index, saw_end = 8, 0, False
|
|
112
|
+
while valid and offset + 12 <= len(data):
|
|
113
|
+
length = int.from_bytes(data[offset:offset + 4], "big")
|
|
114
|
+
end = offset + 12 + length
|
|
115
|
+
if end > len(data):
|
|
116
|
+
valid = False
|
|
117
|
+
break
|
|
118
|
+
kind = data[offset + 4:offset + 8]
|
|
119
|
+
if chunk_index == 0:
|
|
120
|
+
valid = kind == b"IHDR" and length == 13 and int.from_bytes(data[offset + 8:offset + 12], "big") == width and int.from_bytes(data[offset + 12:offset + 16], "big") == height
|
|
121
|
+
if kind == b"IEND":
|
|
122
|
+
saw_end = valid and length == 0 and end == len(data)
|
|
123
|
+
break
|
|
124
|
+
offset, chunk_index = end, chunk_index + 1
|
|
125
|
+
if not valid or not saw_end:
|
|
126
|
+
raise Scene3dNodePngExportError("MALFORMED_NATIVE_OUTPUT", "The native rasterizer returned malformed PNG bytes or unexpected dimensions")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def renderScene3dToPNG(options: Mapping[str, Any]) -> bytes:
|
|
130
|
+
"""Render static Scene3D SVG through the source rasterizer's Rust implementation."""
|
|
131
|
+
if options.get("type") not in {"scatter3d", "line3d", "surface", "mesh3d", "pie3d"}:
|
|
132
|
+
raise Scene3dNodePngExportError("UNSUPPORTED_CHART_TYPE", "renderScene3dToPNG supports only experimental scatter3d, line3d, surface, mesh3d, and pie3d charts")
|
|
133
|
+
width, height = options.get("width"), options.get("height")
|
|
134
|
+
safe_limit = 2**53 - 1 # ECMAScript Number.MAX_SAFE_INTEGER from the source contract.
|
|
135
|
+
if any(isinstance(value, bool) or not isinstance(value, int) or value <= 0 or value > safe_limit for value in (width, height)):
|
|
136
|
+
raise Scene3dNodePngExportError("INVALID_DIMENSIONS", "renderScene3dToPNG requires positive integer width and height values")
|
|
137
|
+
pixels = width * height
|
|
138
|
+
max_pixels = get_defaults("scene3d")["nodePngMaxPixels"]
|
|
139
|
+
if pixels > safe_limit or pixels > max_pixels:
|
|
140
|
+
raise Scene3dNodePngExportError("PIXEL_LIMIT_EXCEEDED", f"renderScene3dToPNG rejects {pixels} pixels; the limit is {max_pixels}")
|
|
141
|
+
from .svg3d import renderScene3dToSVG
|
|
142
|
+
try:
|
|
143
|
+
svg = renderScene3dToSVG(options)
|
|
144
|
+
except Exception as error:
|
|
145
|
+
raise Scene3dNodePngExportError("SVG_RENDER_FAILED", "The renderer-neutral Scene3D SVG could not be generated", error) from error
|
|
146
|
+
if not hasRustPlotEngine():
|
|
147
|
+
raise Scene3dNodePngExportError("NODE_NATIVE_UNAVAILABLE", "renderScene3dToPNG requires the optional fastPlotPy Rust native library")
|
|
148
|
+
try:
|
|
149
|
+
result = b64decode(rustCall("raster_svg", svg=svg, width=width, height=height, maxPixels=max_pixels), validate=True)
|
|
150
|
+
except Exception as error:
|
|
151
|
+
raise Scene3dNodePngExportError("NATIVE_RASTER_FAILED", "The native Scene3D rasterizer failed without returning partial bytes", error) from error
|
|
152
|
+
_validate_png(result, width, height)
|
|
153
|
+
return result
|