frameplot 0.1.0__py3-none-any.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.
@@ -0,0 +1,306 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import xml.etree.ElementTree as ET
5
+
6
+ from frameplot.layout.types import (
7
+ DetailPanelLayout,
8
+ GraphLayout,
9
+ GuideLine,
10
+ LayoutNode,
11
+ LayoutResult,
12
+ Point,
13
+ RoutedEdge,
14
+ )
15
+ from frameplot.theme import Theme
16
+
17
+ SVG_NS = "http://www.w3.org/2000/svg"
18
+ ET.register_namespace("", SVG_NS)
19
+
20
+
21
+ def render_svg(layout: LayoutResult, theme: Theme) -> str:
22
+ """Serialize a computed layout into an SVG document string."""
23
+
24
+ root = ET.Element(
25
+ ET.QName(SVG_NS, "svg"),
26
+ attrib={
27
+ "width": _fmt(layout.width),
28
+ "height": _fmt(layout.height),
29
+ "viewBox": f"0 0 {_fmt(layout.width)} {_fmt(layout.height)}",
30
+ "fill": "none",
31
+ },
32
+ )
33
+
34
+ defs = ET.SubElement(root, ET.QName(SVG_NS, "defs"))
35
+ marker_ids = _build_markers(defs, _all_edges(layout), theme)
36
+
37
+ ET.SubElement(
38
+ root,
39
+ ET.QName(SVG_NS, "rect"),
40
+ attrib={
41
+ "x": "0",
42
+ "y": "0",
43
+ "width": _fmt(layout.width),
44
+ "height": _fmt(layout.height),
45
+ "fill": theme.background_color,
46
+ },
47
+ )
48
+
49
+ group_layer = ET.SubElement(root, ET.QName(SVG_NS, "g"), attrib={"id": "groups"})
50
+ _render_groups(group_layer, layout.main, theme)
51
+
52
+ if layout.detail_panel is not None:
53
+ guide_layer = ET.SubElement(root, ET.QName(SVG_NS, "g"), attrib={"id": "detail-panel-guides"})
54
+ _render_guide_lines(guide_layer, layout.detail_panel.guide_lines, theme)
55
+
56
+ panel_layer = ET.SubElement(
57
+ root,
58
+ ET.QName(SVG_NS, "g"),
59
+ attrib={"id": f"detail-panel-{layout.detail_panel.panel.id}"},
60
+ )
61
+ _render_detail_panel_container(panel_layer, layout.detail_panel, theme)
62
+ _render_groups(panel_layer, layout.detail_panel.graph, theme)
63
+
64
+ edge_layer = ET.SubElement(root, ET.QName(SVG_NS, "g"), attrib={"id": "edges"})
65
+ _render_edges(edge_layer, layout.main.edges, marker_ids, theme)
66
+ if layout.detail_panel is not None:
67
+ _render_edges(edge_layer, layout.detail_panel.graph.edges, marker_ids, theme)
68
+
69
+ node_layer = ET.SubElement(root, ET.QName(SVG_NS, "g"), attrib={"id": "nodes"})
70
+ _render_nodes(node_layer, layout.main.nodes, theme)
71
+ if layout.detail_panel is not None:
72
+ _render_nodes(
73
+ node_layer,
74
+ layout.detail_panel.graph.nodes,
75
+ theme,
76
+ id_prefix=f"detail-panel-{layout.detail_panel.panel.id}-",
77
+ )
78
+
79
+ return ET.tostring(root, encoding="unicode")
80
+
81
+
82
+ def _all_edges(layout: LayoutResult) -> tuple[RoutedEdge, ...]:
83
+ if layout.detail_panel is None:
84
+ return layout.main.edges
85
+ return layout.main.edges + layout.detail_panel.graph.edges
86
+
87
+
88
+ def _render_groups(parent: ET.Element, graph: GraphLayout, theme: Theme) -> None:
89
+ for overlay in graph.groups:
90
+ ET.SubElement(
91
+ parent,
92
+ ET.QName(SVG_NS, "rect"),
93
+ attrib={
94
+ "x": _fmt(overlay.bounds.x),
95
+ "y": _fmt(overlay.bounds.y),
96
+ "width": _fmt(overlay.bounds.width),
97
+ "height": _fmt(overlay.bounds.height),
98
+ "rx": _fmt(theme.group_corner_radius),
99
+ "ry": _fmt(theme.group_corner_radius),
100
+ "stroke": overlay.stroke,
101
+ "stroke-width": _fmt(theme.group_stroke_width),
102
+ "stroke-dasharray": "8 6",
103
+ "fill": overlay.fill,
104
+ "fill-opacity": _fmt(theme.group_fill_opacity),
105
+ },
106
+ )
107
+ ET.SubElement(
108
+ parent,
109
+ ET.QName(SVG_NS, "text"),
110
+ attrib={
111
+ "x": _fmt(overlay.bounds.x + theme.group_padding),
112
+ "y": _fmt(overlay.bounds.y + theme.subtitle_font_size + 6),
113
+ "fill": theme.group_label_color,
114
+ "font-family": theme.title_font_family,
115
+ "font-size": _fmt(theme.subtitle_font_size),
116
+ "font-weight": str(theme.title_font_weight),
117
+ },
118
+ ).text = overlay.group.label
119
+
120
+
121
+ def _render_guide_lines(parent: ET.Element, guide_lines: tuple[GuideLine, ...], theme: Theme) -> None:
122
+ for guide_line in guide_lines:
123
+ ET.SubElement(
124
+ parent,
125
+ ET.QName(SVG_NS, "path"),
126
+ attrib={
127
+ "d": _path_data(guide_line.points),
128
+ "stroke": guide_line.stroke,
129
+ "stroke-width": _fmt(theme.detail_panel_guide_width),
130
+ "stroke-linecap": "round",
131
+ "stroke-linejoin": "round",
132
+ },
133
+ )
134
+
135
+
136
+ def _render_detail_panel_container(
137
+ parent: ET.Element,
138
+ detail_panel: DetailPanelLayout,
139
+ theme: Theme,
140
+ ) -> None:
141
+ ET.SubElement(
142
+ parent,
143
+ ET.QName(SVG_NS, "rect"),
144
+ attrib={
145
+ "x": _fmt(detail_panel.bounds.x),
146
+ "y": _fmt(detail_panel.bounds.y),
147
+ "width": _fmt(detail_panel.bounds.width),
148
+ "height": _fmt(detail_panel.bounds.height),
149
+ "rx": _fmt(theme.detail_panel_corner_radius),
150
+ "ry": _fmt(theme.detail_panel_corner_radius),
151
+ "stroke": detail_panel.stroke,
152
+ "stroke-width": _fmt(theme.detail_panel_stroke_width),
153
+ "fill": detail_panel.fill,
154
+ "fill-opacity": _fmt(theme.detail_panel_fill_opacity),
155
+ },
156
+ )
157
+ ET.SubElement(
158
+ parent,
159
+ ET.QName(SVG_NS, "text"),
160
+ attrib={
161
+ "x": _fmt(detail_panel.bounds.x + theme.detail_panel_padding),
162
+ "y": _fmt(detail_panel.bounds.y + theme.subtitle_font_size + 8),
163
+ "fill": theme.detail_panel_title_color,
164
+ "font-family": theme.title_font_family,
165
+ "font-size": _fmt(theme.subtitle_font_size),
166
+ "font-weight": str(theme.title_font_weight),
167
+ },
168
+ ).text = detail_panel.panel.label
169
+
170
+
171
+ def _render_edges(
172
+ parent: ET.Element,
173
+ edges: tuple[RoutedEdge, ...],
174
+ marker_ids: dict[str, str],
175
+ theme: Theme,
176
+ ) -> None:
177
+ for routed_edge in edges:
178
+ attributes = {
179
+ "d": _path_data(routed_edge.points),
180
+ "stroke": routed_edge.stroke,
181
+ "stroke-width": _fmt(theme.stroke_width),
182
+ "stroke-linecap": "round",
183
+ "stroke-linejoin": "round",
184
+ "marker-end": f"url(#{marker_ids[routed_edge.stroke]})",
185
+ }
186
+ if routed_edge.edge.dashed:
187
+ attributes["stroke-dasharray"] = "8 6"
188
+ ET.SubElement(parent, ET.QName(SVG_NS, "path"), attrib=attributes)
189
+
190
+
191
+ def _render_nodes(
192
+ parent: ET.Element,
193
+ nodes: dict[str, LayoutNode],
194
+ theme: Theme,
195
+ *,
196
+ id_prefix: str = "",
197
+ ) -> None:
198
+ for node_id in sorted(nodes, key=lambda item: (nodes[item].rank, nodes[item].order, item)):
199
+ _render_node(parent, nodes[node_id], theme, element_id=f"{id_prefix}{node_id}")
200
+
201
+
202
+ def _render_node(
203
+ parent: ET.Element,
204
+ layout_node: LayoutNode,
205
+ theme: Theme,
206
+ *,
207
+ element_id: str,
208
+ ) -> None:
209
+ node = layout_node.node
210
+ node_group = ET.SubElement(parent, ET.QName(SVG_NS, "g"), attrib={"id": element_id})
211
+ ET.SubElement(
212
+ node_group,
213
+ ET.QName(SVG_NS, "rect"),
214
+ attrib={
215
+ "x": _fmt(layout_node.x),
216
+ "y": _fmt(layout_node.y),
217
+ "width": _fmt(layout_node.width),
218
+ "height": _fmt(layout_node.height),
219
+ "rx": _fmt(theme.corner_radius),
220
+ "ry": _fmt(theme.corner_radius),
221
+ "fill": node.fill or theme.node_fill,
222
+ "stroke": node.stroke or theme.node_stroke,
223
+ "stroke-width": _fmt(theme.stroke_width),
224
+ },
225
+ )
226
+
227
+ text_x = layout_node.x + theme.node_padding_x
228
+ content_top = layout_node.y + (layout_node.height - layout_node.content_height) / 2
229
+ current_top = content_top
230
+ title_ascent = theme.title_font_size * 0.8
231
+ subtitle_ascent = theme.subtitle_font_size * 0.8
232
+ text_color = node.text_color or theme.node_text_color
233
+
234
+ for line in layout_node.title_lines:
235
+ current_top += layout_node.title_line_height
236
+ ET.SubElement(
237
+ node_group,
238
+ ET.QName(SVG_NS, "text"),
239
+ attrib={
240
+ "x": _fmt(text_x),
241
+ "y": _fmt(current_top - layout_node.title_line_height + title_ascent),
242
+ "fill": text_color,
243
+ "font-family": theme.title_font_family,
244
+ "font-size": _fmt(theme.title_font_size),
245
+ "font-weight": str(theme.title_font_weight),
246
+ },
247
+ ).text = line
248
+
249
+ if layout_node.subtitle_lines:
250
+ current_top += theme.inter_text_gap
251
+ for line in layout_node.subtitle_lines:
252
+ current_top += layout_node.subtitle_line_height
253
+ ET.SubElement(
254
+ node_group,
255
+ ET.QName(SVG_NS, "text"),
256
+ attrib={
257
+ "x": _fmt(text_x),
258
+ "y": _fmt(current_top - layout_node.subtitle_line_height + subtitle_ascent),
259
+ "fill": text_color,
260
+ "font-family": theme.title_font_family,
261
+ "font-size": _fmt(theme.subtitle_font_size),
262
+ "font-weight": str(theme.subtitle_font_weight),
263
+ },
264
+ ).text = line
265
+
266
+
267
+ def _build_markers(defs: ET.Element, edges: tuple[RoutedEdge, ...], theme: Theme) -> dict[str, str]:
268
+ marker_ids: dict[str, str] = {}
269
+ for color in sorted({routed_edge.stroke for routed_edge in edges}):
270
+ marker_id = f"arrow-{hashlib.sha1(color.encode('utf-8')).hexdigest()[:10]}"
271
+ marker_ids[color] = marker_id
272
+ marker = ET.SubElement(
273
+ defs,
274
+ ET.QName(SVG_NS, "marker"),
275
+ attrib={
276
+ "id": marker_id,
277
+ "markerWidth": _fmt(theme.arrow_size),
278
+ "markerHeight": _fmt(theme.arrow_size),
279
+ "refX": _fmt(theme.arrow_size),
280
+ "refY": _fmt(theme.arrow_size / 2),
281
+ "orient": "auto",
282
+ "markerUnits": "userSpaceOnUse",
283
+ },
284
+ )
285
+ ET.SubElement(
286
+ marker,
287
+ ET.QName(SVG_NS, "path"),
288
+ attrib={
289
+ "d": f"M0,0 L{_fmt(theme.arrow_size)},{_fmt(theme.arrow_size / 2)} L0,{_fmt(theme.arrow_size)} z",
290
+ "fill": color,
291
+ },
292
+ )
293
+ return marker_ids
294
+
295
+
296
+ def _path_data(points: tuple[Point, ...]) -> str:
297
+ commands = [f"M {_fmt(points[0].x)} {_fmt(points[0].y)}"]
298
+ commands.extend(f"L {_fmt(point.x)} {_fmt(point.y)}" for point in points[1:])
299
+ return " ".join(commands)
300
+
301
+
302
+ def _fmt(value: float) -> str:
303
+ text = f"{value:.2f}"
304
+ if "." in text:
305
+ text = text.rstrip("0").rstrip(".")
306
+ return text or "0"
frameplot/theme.py ADDED
@@ -0,0 +1,59 @@
1
+ """Styling defaults for frameplot layout and rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class Theme:
10
+ """Control colors, typography, spacing, and routing defaults.
11
+
12
+ A theme applies global defaults to the whole diagram. Per-node, per-edge,
13
+ per-group, and per-detail-panel overrides still win when provided.
14
+ """
15
+
16
+ background_color: str = "#FFFFFF"
17
+ node_fill: str = "#F8FAFC"
18
+ node_stroke: str = "#0F172A"
19
+ node_text_color: str = "#0F172A"
20
+ edge_color: str = "#334155"
21
+ group_stroke: str = "#94A3B8"
22
+ group_fill: str = "#E2E8F0"
23
+ group_label_color: str = "#475569"
24
+ title_font_family: str = "DejaVu Sans, Arial, sans-serif"
25
+ title_font_size: float = 16.0
26
+ subtitle_font_size: float = 12.0
27
+ title_font_weight: int = 600
28
+ subtitle_font_weight: int = 400
29
+ outer_margin: float = 32.0
30
+ node_padding_x: float = 18.0
31
+ node_padding_y: float = 14.0
32
+ inter_text_gap: float = 8.0
33
+ rank_gap: float = 96.0
34
+ node_gap: float = 32.0
35
+ component_gap: float = 64.0
36
+ group_padding: float = 18.0
37
+ max_text_width: float = 220.0
38
+ min_node_width: float = 140.0
39
+ min_node_height: float = 72.0
40
+ corner_radius: float = 16.0
41
+ group_corner_radius: float = 20.0
42
+ stroke_width: float = 2.0
43
+ group_stroke_width: float = 1.5
44
+ group_fill_opacity: float = 0.14
45
+ detail_panel_gap: float = 48.0
46
+ detail_panel_padding: float = 20.0
47
+ detail_panel_header_height: float = 30.0
48
+ detail_panel_fill: str = "#FFFFFF"
49
+ detail_panel_stroke: str = "#CBD5E1"
50
+ detail_panel_title_color: str = "#475569"
51
+ detail_panel_stroke_width: float = 1.2
52
+ detail_panel_fill_opacity: float = 1.0
53
+ detail_panel_corner_radius: float = 18.0
54
+ detail_panel_guide_color: str = "#CBD5E1"
55
+ detail_panel_guide_width: float = 1.2
56
+ arrow_size: float = 8.0
57
+ route_track_gap: float = 18.0
58
+ back_edge_gap: float = 26.0
59
+ self_loop_size: float = 28.0
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: frameplot
3
+ Version: 0.1.0
4
+ Summary: Turn Python-defined pipeline graphs into presentation-ready SVG and PNG diagrams.
5
+ Author: Small Turtle 2
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/smturtle2/frameplot
8
+ Project-URL: Repository, https://github.com/smturtle2/frameplot
9
+ Project-URL: Issues, https://github.com/smturtle2/frameplot/issues
10
+ Project-URL: Releases, https://github.com/smturtle2/frameplot/releases
11
+ Keywords: diagram,graph,pipeline,png,svg,visualization
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Topic :: Scientific/Engineering :: Visualization
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: CairoSVG>=2.7
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # frameplot
31
+
32
+ [![PyPI version](https://img.shields.io/pypi/v/frameplot.svg)](https://pypi.org/project/frameplot/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/frameplot.svg)](https://pypi.org/project/frameplot/)
34
+ [![CI](https://github.com/smturtle2/frameplot/actions/workflows/workflow.yml/badge.svg?branch=main)](https://github.com/smturtle2/frameplot/actions/workflows/workflow.yml)
35
+ [![License](https://img.shields.io/github/license/smturtle2/frameplot)](https://github.com/smturtle2/frameplot/blob/main/LICENSE)
36
+
37
+ Turn Python-defined pipeline graphs into presentation-ready SVG and PNG diagrams.
38
+
39
+ [한국어 README](https://github.com/smturtle2/frameplot/blob/main/README.ko.md)
40
+
41
+ ![frameplot hero image](https://raw.githubusercontent.com/smturtle2/frameplot/main/docs/assets/frameplot-hero.png)
42
+
43
+ `frameplot` is a compact Python library for rendering left-to-right pipeline diagrams with clean defaults. Define nodes, edges, groups, and optional detail panels in plain Python, then export polished SVG for documentation or PNG for slides and papers.
44
+
45
+ ## Why frameplot?
46
+
47
+ - Clean left-to-right layout for architecture diagrams, data pipelines, and model overviews
48
+ - SVG-first output with optional PNG export through CairoSVG
49
+ - Detail panels for expanding a summary node into a lower inset mini-graph
50
+ - Themeable typography, spacing, colors, and routing defaults
51
+ - Deterministic rendering from simple dataclass-based inputs
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ python -m pip install frameplot
57
+ ```
58
+
59
+ PNG export depends on CairoSVG and may require Cairo or libffi packages from the host OS.
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from frameplot import Edge, Group, Node, Pipeline
65
+
66
+ pipeline = Pipeline(
67
+ nodes=[
68
+ Node("start", "Start", "Receive request"),
69
+ Node("fetch", "Fetch Data", "Load source tables"),
70
+ Node("retry", "Retry", "Loop on transient failure", fill="#FFF2CC"),
71
+ Node("done", "Done", "Return result", fill="#D9EAD3"),
72
+ ],
73
+ edges=[
74
+ Edge("e1", "start", "fetch"),
75
+ Edge("e2", "fetch", "retry", dashed=True),
76
+ Edge("e3", "retry", "fetch", color="#C0504D"),
77
+ Edge("e4", "fetch", "done"),
78
+ ],
79
+ groups=[
80
+ Group("g1", "Execution", ["start", "fetch", "retry"], edge_ids=["e2"]),
81
+ ],
82
+ )
83
+
84
+ svg = pipeline.to_svg()
85
+ pipeline.save_svg("pipeline.svg")
86
+ pipeline.save_png("pipeline.png")
87
+ ```
88
+
89
+ ## Public API
90
+
91
+ Top-level imports are the supported public API:
92
+
93
+ - `Node(id, title, subtitle=None, fill=None, stroke=None, text_color=None, metadata=None, width=None, height=None)`
94
+ - `Edge(id, source, target, color=None, dashed=False, metadata=None)`
95
+ - `Group(id, label, node_ids, edge_ids=(), stroke=None, fill=None, metadata=None)`
96
+ - `DetailPanel(id, focus_node_id, label, nodes, edges, groups=(), stroke=None, fill=None, metadata=None)`
97
+ - `Theme(...)`
98
+ - `Pipeline(nodes, edges, groups=(), detail_panel=None, theme=None)`
99
+
100
+ `Pipeline` exposes:
101
+
102
+ - `to_svg() -> str`
103
+ - `save_svg(path) -> None`
104
+ - `to_png_bytes() -> bytes`
105
+ - `save_png(path) -> None`
106
+
107
+ ## Advanced Example
108
+
109
+ The hero image above is generated from [`examples/sar_backbone_example.py`](https://github.com/smturtle2/frameplot/blob/main/examples/sar_backbone_example.py), which demonstrates:
110
+
111
+ - custom `Theme` values
112
+ - split decoder branches
113
+ - grouped overlays
114
+ - a `DetailPanel` attached to a summary node
115
+
116
+ ## Design Notes
117
+
118
+ - Layout is intentionally left-to-right in v0.x.
119
+ - Edge labels are not supported yet.
120
+ - Groups are visual overlays and do not constrain layout.
121
+ - Detail panels render as separate lower insets attached to a focus node in the main flow.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ python -m venv .venv
127
+ source .venv/bin/activate
128
+ python -m pip install -e '.[dev]'
129
+ python -m pytest -q
130
+ ```
131
+
132
+ Release publishing is automated through GitHub Actions and PyPI Trusted Publishing. Bump the version in `pyproject.toml`, create a tag like `v0.1.0`, and push the tag to trigger a release from `.github/workflows/workflow.yml`.
@@ -0,0 +1,21 @@
1
+ frameplot/__init__.py,sha256=sFaxPWRSlnA0HjBf-1XCL2CI9GSMVD3AjBop5Ec1Lio,266
2
+ frameplot/api.py,sha256=LTjY3qg1nY0rhB4PtNi1IrD4jypneaLQvdoFrDY9dWE,1865
3
+ frameplot/model.py,sha256=3ruaNf36JfaQcxtAUYQtY0jupbLaf2wxxxHoAvVbuCQ,4371
4
+ frameplot/theme.py,sha256=D2ujyFIuVf7I7AN-iZYaZN2KJxsw-ySnneusce6bZU4,1984
5
+ frameplot/layout/__init__.py,sha256=AfmzjPLX_Vqv_Rvm-SlhNVGZmMc2gHVim2Eupl0BV6w,10040
6
+ frameplot/layout/order.py,sha256=pRvH6Yitem_29slUAbblcXPPV0UTccAwoLGaB-QZ9Uk,5585
7
+ frameplot/layout/place.py,sha256=_hM92R0LS5-rcdLvF427VxnAxWwlnnkfmkCboRmBzZc,7091
8
+ frameplot/layout/rank.py,sha256=T95P10lAV-3hc-T-uQeBJ7zxnLILg7ihBVpPzzsILHM,2279
9
+ frameplot/layout/route.py,sha256=nuu64g7E1byBvyVoyGHMBdQZglVIUFJ1QNCmk64MfbQ,33086
10
+ frameplot/layout/scc.py,sha256=D6fWWvbcb62eKDxDhqJoxuPGQj93tPNV8x6i5jYHvo0,2216
11
+ frameplot/layout/text.py,sha256=pT9EYm0vtcr1ipuOhHzchHjh2wYY4yRyyuGzkrzjOd0,2587
12
+ frameplot/layout/types.py,sha256=v_4Ld_Eb4zBP5P-1q3DH89o9eGlaGeuWYWPvwPIqBqw,3524
13
+ frameplot/layout/validate.py,sha256=rLhjzoqMtXY7nfwDbwIHJoqkAqEFmzxad1uZGP8jxW8,3977
14
+ frameplot/render/__init__.py,sha256=-HFtKujLTvZKcY9ZFIn9-5zRWJTnnmDheUkvC2Ln9qA,162
15
+ frameplot/render/png.py,sha256=p-aGIa0hTJTOfLm70pNqdTDHVqwNBDYiHDKqo0ZGvy4,587
16
+ frameplot/render/svg.py,sha256=-oHav_7S8bart9vGIPa6UmF3X5ws5SJsK5Hmo0kWkG8,10701
17
+ frameplot-0.1.0.dist-info/licenses/LICENSE,sha256=K6I_4F9xoqE32M-CpGHLV7WmetFHWZwj60bJ5X4bbR0,1071
18
+ frameplot-0.1.0.dist-info/METADATA,sha256=nuh72IvnII6txTM7neEBi2apTRskwRJO2VXOTZ2xxf0,5148
19
+ frameplot-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ frameplot-0.1.0.dist-info/top_level.txt,sha256=yQ12_9t2dlReyty9T1ahzWzWNPywPZ6WZ3zwBrlPsxg,10
21
+ frameplot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Small Turtle 2
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.
@@ -0,0 +1 @@
1
+ frameplot