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.
Files changed (78) hide show
  1. fastplotpy/README.md +70 -0
  2. fastplotpy/__init__.py +33 -0
  3. fastplotpy/assets/dist/assets/scene.worker-CcnexaKr.js +1 -0
  4. fastplotpy/assets/dist/index.js +15071 -0
  5. fastplotpy/assets/native/NotoSans-OFL.txt +96 -0
  6. fastplotpy/assets/native/THIRD_PARTY_NOTICES.md +24 -0
  7. fastplotpy/assets/native/fastplot_python_abi.dll +0 -0
  8. fastplotpy/assets/package.json +1 -0
  9. fastplotpy/axes_validation.py +102 -0
  10. fastplotpy/camera3d.py +104 -0
  11. fastplotpy/color_scale.py +88 -0
  12. fastplotpy/defaults/area-fill.json +5 -0
  13. fastplotpy/defaults/bar.json +22 -0
  14. fastplotpy/defaults/camera3d.json +22 -0
  15. fastplotpy/defaults/cartesian-references.json +10 -0
  16. fastplotpy/defaults/control.json +3 -0
  17. fastplotpy/defaults/crosshair.json +19 -0
  18. fastplotpy/defaults/error-bars.json +7 -0
  19. fastplotpy/defaults/heatmap.json +70 -0
  20. fastplotpy/defaults/interaction.json +3 -0
  21. fastplotpy/defaults/layout.json +32 -0
  22. fastplotpy/defaults/legend.json +24 -0
  23. fastplotpy/defaults/line-axes.json +28 -0
  24. fastplotpy/defaults/line3d.json +13 -0
  25. fastplotpy/defaults/markers.json +6 -0
  26. fastplotpy/defaults/mesh3d.json +13 -0
  27. fastplotpy/defaults/pan.json +7 -0
  28. fastplotpy/defaults/pie.json +27 -0
  29. fastplotpy/defaults/pie3d.json +7 -0
  30. fastplotpy/defaults/point-labels.json +13 -0
  31. fastplotpy/defaults/pointClick.json +6 -0
  32. fastplotpy/defaults/pointSelection.json +13 -0
  33. fastplotpy/defaults/polar.json +71 -0
  34. fastplotpy/defaults/polarRotate.json +5 -0
  35. fastplotpy/defaults/scatter3d.json +39 -0
  36. fastplotpy/defaults/scene3d.json +61 -0
  37. fastplotpy/defaults/surface.json +35 -0
  38. fastplotpy/defaults/tooltip.json +18 -0
  39. fastplotpy/defaults/trace-colors.json +10 -0
  40. fastplotpy/defaults/trace-options.json +24 -0
  41. fastplotpy/defaults/wonderplot.json +58 -0
  42. fastplotpy/defaults/zoom.json +12 -0
  43. fastplotpy/defaults.py +15 -0
  44. fastplotpy/error_bars.py +118 -0
  45. fastplotpy/exports.py +153 -0
  46. fastplotpy/families2d.py +896 -0
  47. fastplotpy/heatmap_labels.py +146 -0
  48. fastplotpy/interactions.py +243 -0
  49. fastplotpy/interactive.py +1922 -0
  50. fastplotpy/js_serialize.py +47 -0
  51. fastplotpy/lighting3d.py +113 -0
  52. fastplotpy/line3d_lod.py +138 -0
  53. fastplotpy/lod3d.py +105 -0
  54. fastplotpy/mesh3d_lod.py +107 -0
  55. fastplotpy/native.py +163 -0
  56. fastplotpy/overlays.py +561 -0
  57. fastplotpy/point_labels.py +86 -0
  58. fastplotpy/polar.py +445 -0
  59. fastplotpy/polygon_clip3d.py +148 -0
  60. fastplotpy/references.py +72 -0
  61. fastplotpy/rendering.py +65 -0
  62. fastplotpy/rust_scenes.py +314 -0
  63. fastplotpy/scene.py +1185 -0
  64. fastplotpy/scene3d.py +1215 -0
  65. fastplotpy/scene3d_accessibility.py +183 -0
  66. fastplotpy/scene3d_collection.py +158 -0
  67. fastplotpy/subplots.py +115 -0
  68. fastplotpy/surface_contours.py +122 -0
  69. fastplotpy/surface_lod.py +109 -0
  70. fastplotpy/svg3d.py +235 -0
  71. fastplotpy/trendline.py +236 -0
  72. fastplotpy/triangle_wireframe.py +51 -0
  73. fastplotpy/types.py +3027 -0
  74. fastplotpy-0.3.0a1.dist-info/METADATA +86 -0
  75. fastplotpy-0.3.0a1.dist-info/RECORD +78 -0
  76. fastplotpy-0.3.0a1.dist-info/WHEEL +5 -0
  77. fastplotpy-0.3.0a1.dist-info/licenses/LICENSE +21 -0
  78. 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