figgyplot 0.2.1__cp315-cp315-win_arm64.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.
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,29 @@
1
+ # References and licenses
2
+
3
+ The integration follows the public model, picking, drag/resize, export and SSOT
4
+ contracts in shim9610/figgy (reference HEAD 84aa2783caa97d679c72b5c079fb4aea8bf1040e)
5
+ and the separation of accepted model state from UI drafts in shim9610/figgy-web
6
+ (reference HEAD b15290af0f5ca83c94bf60b066f9565b7e0d878c). Their renderer code and
7
+ Studio application are not copied into this package.
8
+
9
+ figgy-renderer and its bundled fonts retain their MIT/Apache-2.0 and OFL notices.
10
+ The font notices are shipped in `figgy/licenses/LICENSE-LiberationSans.txt` and
11
+ `figgy/licenses/LICENSE-ComicNeue.txt`, including when fonts are embedded in the
12
+ native extension.
13
+ The native desktop uses winit/wgpu and egui. PySide6 is optional benchmark-only tooling.
14
+ The notebook extra uses anywidget and ipywidgets under their respective licenses.
15
+
16
+ ## Packaged browser kernel
17
+
18
+ Generated JS/WASM assets use upstream `figgy` web package 0.13.1 and
19
+ `figgy-renderer` 0.16.1 at commit
20
+ `84aa2783caa97d679c72b5c079fb4aea8bf1040e`, under MIT OR Apache-2.0.
21
+ Rebuild path and asset hashes are documented in `docs/HOSTS.md` and
22
+ `python/figgyplot/assets/provenance.json`. The same bundled font licenses apply.
23
+
24
+ ## Desktop UI fonts
25
+
26
+ The native egui UI embeds `epaint_default_fonts 0.36.2`'s Hack Regular,
27
+ Ubuntu Light, Noto Emoji Regular and emoji-icon-font. Their complete font
28
+ license texts are included as `python/figgyplot/licenses/LICENSE-Egui-*.txt`
29
+ (Hack/Bitstream terms, Ubuntu Font Licence, SIL OFL and icon-font MIT).
figgyplot/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """NumPy-first plots; Rust owns rendering, picking and accepted settings."""
2
+ from .figure import Figure, Axes, Artist, RadialArtist, figure, subplots, text_segments
3
+ from ._native import release_gpu
4
+
5
+ __version__ = "0.2.1"
6
+ __all__ = ["Figure", "Axes", "Artist", "RadialArtist", "figure", "subplots", "text_segments", "release_gpu"]
figgyplot/_codegen.py ADDED
@@ -0,0 +1,156 @@
1
+ """Readable Python export, shared by native and notebook editors (no GPU access)."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import json
6
+ import keyword
7
+ import pprint
8
+
9
+
10
+ def settings_to_code(settings, defaults, dpi=120, variable="fig"):
11
+ from .figure import _code_tree, _diff, _segments
12
+
13
+ if not isinstance(variable, str) or not variable.isidentifier() or keyword.iskeyword(variable):
14
+ raise ValueError("variable must be a Python identifier")
15
+ if isinstance(settings, str):
16
+ settings = json.loads(settings)
17
+ if isinstance(defaults, str):
18
+ defaults = json.loads(defaults)
19
+ if settings.get("radial") is not None:
20
+ chart = copy.deepcopy(settings["radial"])
21
+ chart["interaction"] = {"hovered": None, "selected": None, "hover_progress": 0.0}
22
+ chart["decorations"]["selected"] = None
23
+ slices = {i: {key: value for key, value in item.items() if key != "value"}
24
+ for i, item in enumerate(chart.pop("slices"))}
25
+ chart["slices"] = slices
26
+ args = pprint.pformat(chart, width=96, sort_dicts=False)
27
+ canvas = pprint.pformat({"chart_area": settings["config"]["chart_area"]})
28
+ return (f"# Apply to an existing pie/donut with the same slice count and DPI.\n"
29
+ f"# Existing category values are preserved.\n"
30
+ f"{variable}.configure(config={canvas})\n"
31
+ f"{variable}.configure_radial(expected_slices={len(slices)}, **{args})\n")
32
+ segments_function = "_text_segments" if variable == "text_segments" else "text_segments"
33
+ cfg = settings["config"]
34
+ modeled = copy.deepcopy(defaults)
35
+ lines = [f"# Apply to the existing {variable} with the same plotted series.",
36
+ f"ax = {variable}.axes[0]" if variable != "ax" else "_ax = ax.axes[0]",
37
+ f"{variable}.configure(reset=True)"]
38
+ axis = "_ax" if variable == "ax" else "ax"
39
+ if "draw_style" in cfg:
40
+ draw_style = copy.deepcopy(cfg["draw_style"])
41
+ mode = draw_style.pop("mode")
42
+ options = ", ".join(f"{key}={value!r}" for key, value in draw_style.items())
43
+ lines.append(f"{variable}.set_draw_style({mode!r}" + (f", {options}" if options else "") + ")")
44
+ modeled["draw_style"] = copy.deepcopy(cfg["draw_style"])
45
+ if settings.get("background") is not None:
46
+ color = tuple(settings["background"][key] for key in "rgba")
47
+ lines.append(f"{variable}.set_facecolor({color!r})")
48
+
49
+ for key, method in (("chart_title", "set_title"), ("bottom_x", "set_xlabel"), ("left_y", "set_ylabel")):
50
+ node = cfg[key] if key == "chart_title" else cfg[key]["title_option"]
51
+ baseline = modeled[key] if key == "chart_title" else modeled[key]["title_option"]
52
+ rich = node["text"]
53
+ value = "".join(s["text"] for s in rich["segments"])
54
+ args = [repr(value)]
55
+ baseline["text"]["segments"] = _segments(value)
56
+ # Python's font sizes are points; the renderer/editor stores pixels.
57
+ for field, prop, val in (("font_size", "fontsize", rich["font_size"] * 72 / dpi),
58
+ ("font", "font", rich["font"]),
59
+ ("color", "color", tuple(rich["color"][c] for c in "rgba"))):
60
+ if field == "font_size" or rich[field] != baseline["text"][field]:
61
+ args.append(f"{prop}={val!r}")
62
+ baseline["text"][field] = copy.deepcopy(rich[field])
63
+ if node["visible"] != baseline["visible"]:
64
+ args.append(f"visible={node['visible']!r}")
65
+ baseline["visible"] = node["visible"]
66
+ offset = (node["offset_x"], node["offset_y"])
67
+ if offset != (baseline["offset_x"], baseline["offset_y"]):
68
+ args.append(f"offset={offset!r}")
69
+ baseline["offset_x"], baseline["offset_y"] = offset
70
+ lines.append(f"{axis}.{method}({', '.join(args)})")
71
+
72
+ for dimension, keys in (("x", ("bottom_x", "top_x")), ("y", ("left_y", "right_y"))):
73
+ primary = cfg[keys[0]]
74
+ lines.append(f"{axis}.set_{dimension}lim({primary['min']!r}, {primary['max']!r})")
75
+ for key in keys:
76
+ modeled[key]["min"], modeled[key]["max"] = primary["min"], primary["max"]
77
+ if primary["scale"] in ("Linear", "Logarithmic") and all(cfg[k]["scale"] == primary["scale"] for k in keys):
78
+ scale = "log" if primary["scale"] == "Logarithmic" else "linear"
79
+ lines.append(f"{axis}.set_{dimension}scale({scale!r})")
80
+ for key in keys:
81
+ modeled[key]["scale"] = primary["scale"]
82
+ modeled[key]["major_spacing"] = 1.0 if scale == "log" else (primary["max"] - primary["min"]) / 5
83
+ if cfg["grid"]["show_major_x"] == cfg["grid"]["show_major_y"]:
84
+ visible = cfg["grid"]["show_major_x"]
85
+ lines.append(f"{axis}.grid({visible!r})")
86
+ modeled["grid"]["show_major_x"] = modeled["grid"]["show_major_y"] = visible
87
+
88
+ series = {}
89
+ for spec in settings["series"]:
90
+ sid = spec["series_id"]
91
+ patch = {k: copy.deepcopy(v) for k, v in spec.items()
92
+ if k not in ("series_id", "x_column", "y_column")}
93
+ style = next(iter(patch["render_type"].values()))
94
+ properties = {}
95
+ colors = [(style[k], field) for k, field in
96
+ (("line", "line_color"), ("scatter", "point_color"), ("bar", "bar_color")) if k in style]
97
+ if colors and all(node[field] == colors[0][0][colors[0][1]] for node, field in colors):
98
+ color = colors[0][0][colors[0][1]]
99
+ properties["color"] = tuple(color[c] for c in "rgba")
100
+ for node, field in colors:
101
+ del node[field]
102
+ if "line" in style:
103
+ line = style["line"]
104
+ properties["linewidth"] = line.pop("line_width") * 72 / dpi
105
+ properties["linestyle"] = {"Solid": "-", "Dash": "--", "Dot": ":", "DashDot": "-."}.get(line["line_style"], line["line_style"])
106
+ del line["line_style"]
107
+ if "scatter" in style:
108
+ properties["markersize"] = style["scatter"].pop("point_size") * 144 / dpi
109
+ if patch.get("label") is not None:
110
+ label = patch["label"]
111
+ value = "".join(s["text"] for s in label["segments"])
112
+ properties["label"] = value
113
+ if label["segments"] == _segments(value):
114
+ del label["segments"]
115
+ if properties:
116
+ args = ", ".join(f"{key}={value!r}" for key, value in properties.items())
117
+ lines.append(f"{variable}.update({sid!r}, {args})")
118
+
119
+ def compact(value):
120
+ if not isinstance(value, dict):
121
+ return value
122
+ return {key: item for key, raw in value.items() if (item := compact(raw)) != {}}
123
+
124
+ patch = compact(patch)
125
+ if patch:
126
+ series[sid] = patch
127
+
128
+ def configure(options):
129
+ lines.append(f"{variable}.configure(")
130
+ for key, value in options.items():
131
+ formatted = pprint.pformat(_code_tree(value, segments_function), width=96, sort_dicts=False)
132
+ lines.append(f" {key}=" + formatted.replace("\n", "\n ") + ",")
133
+ lines.append(")")
134
+
135
+ # Restore advanced series styling before rebuilding the legend so ordinary
136
+ # labels and renderer-generated symbols need no serialized segment list.
137
+ if series:
138
+ lines += ["", "# Preserve advanced series styling."]
139
+ configure({"series": series})
140
+ lines.append(f"{axis}.legend(visible={cfg['legend']['visible']!r})")
141
+ from ._native import rebuild_legend_json
142
+ modeled["legend"]["content"] = json.loads(rebuild_legend_json(
143
+ json.dumps(settings["series"]), json.dumps(modeled["legend"]["content"])))
144
+ modeled["legend"]["visible"] = cfg["legend"]["visible"]
145
+
146
+ # Advanced geometry and rich text remain ordinary Python values.
147
+ # No escaped JSON, data uploads or private renderer IDs are emitted.
148
+ residual = _diff(modeled, cfg)
149
+ residual["chart_area"] = copy.deepcopy(cfg["chart_area"])
150
+ lines += ["", "# Preserve advanced layout."]
151
+ configure({"config": residual})
152
+ code = "\n".join(lines) + "\n"
153
+ if f"{segments_function}(" in code:
154
+ alias = "" if segments_function == "text_segments" else f" as {segments_function}"
155
+ code = f"from figgyplot import text_segments{alias}\n\n" + code
156
+ return code
Binary file