plotly-print 0.2.2__tar.gz → 0.2.3__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.
- plotly_print-0.2.3/.cursor/rules/global.mdc +52 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.cursor/rules/javascript.mdc +2 -1
- plotly_print-0.2.3/.cursor/rules/map.mdc +34 -0
- plotly_print-0.2.3/.cursorignore +2 -0
- plotly_print-0.2.3/AGENTS.md +17 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/PKG-INFO +1 -1
- {plotly_print-0.2.2 → plotly_print-0.2.3}/pyproject.toml +1 -1
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/engine.py +66 -2
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_json_types.py +58 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/uv.lock +1 -1
- plotly_print-0.2.2/.cursor/rules/artifacts.mdc +0 -14
- plotly_print-0.2.2/.cursor/rules/global.mdc +0 -72
- plotly_print-0.2.2/AGENTS.md +0 -15
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.cursor/rules/markdown.mdc +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.github/workflows/check-tag.yml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.github/workflows/publish.yml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.github/workflows/quality-checks.yml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.github/workflows/version-check.yml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.gitignore +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.mdformat.toml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/.pre-commit-config.yaml +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/CONTRIBUTING.md +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/LICENSE +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/README.md +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/THIRD_PARTY_NOTICES.md +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/benchmark_summary.json +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_annotations_arrowheads.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_annotations_long_arrows.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_bar.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_barpolar.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_box.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_candlestick.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_carpet.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_contour.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_funnel.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_funnelarea.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_heatmap.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_histogram.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_histogram2d.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_icicle.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_indicator.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_ohlc.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_parcats.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_pie.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_rotated_ticks_45.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_rotated_ticks_90.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_sankey.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_scatter_line.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_scatterpolar.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_scatterternary.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_sunburst.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_table.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_table_markup.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_treemap.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_violin.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_waterfall.png +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/__init__.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/__main__.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/cases.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/collage.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/compare.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/report.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/benchmark/runner.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/__init__.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/core.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/fonts.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/js/micro_dom.js +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/js/plotly_comprehensive_2d.js +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/plotly_hook.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/src/plotly_print/raster.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_background.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_bar.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_cartesian_types.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_comprehensive_2d.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_heatmap.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_kaleido_visual.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_legend.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_pie.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_plotly_hook.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_raster.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_scatter.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_svg_path_geometry.py +0 -0
- {plotly_print-0.2.2 → plotly_print-0.2.3}/tests/test_svg_transforms.py +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Coding standards for plotly-print (Python package + QuickJS micro-DOM)
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project standards
|
|
7
|
+
|
|
8
|
+
Python package + QuickJS micro-DOM under `src/plotly_print/js/`. Source of truth: `pyproject.toml` (Python >= 3.12, uv). Package layout and entry points: `.cursor/rules/map.mdc`.
|
|
9
|
+
|
|
10
|
+
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
|
11
|
+
|
|
12
|
+
## Before writing code
|
|
13
|
+
|
|
14
|
+
Read the task and the code it touches, trace the real flow end to end, then stop at the first rung that holds:
|
|
15
|
+
|
|
16
|
+
1. Does this need to be built at all? (YAGNI)
|
|
17
|
+
2. Does it already exist in this codebase? Reuse it.
|
|
18
|
+
3. Does the standard library already do this? Use it.
|
|
19
|
+
4. Does an already-declared dependency solve it? Use it.
|
|
20
|
+
5. Can this be one boring line? Make it one line.
|
|
21
|
+
6. Only then: write the minimum code that works.
|
|
22
|
+
|
|
23
|
+
Bug fix = root cause, not symptom. Grep every caller of the function you touch and fix the shared function once.
|
|
24
|
+
|
|
25
|
+
## Toolchain
|
|
26
|
+
|
|
27
|
+
- Do not add a dependency unless it is declared in `pyproject.toml` (or the lockfile). Prefer stdlib.
|
|
28
|
+
- Ruff via `pyproject.toml`. After Python edits: `poe format`. After Markdown: `poe format_docs`.
|
|
29
|
+
- No JS formatter/linter; match `micro_dom.js`. Never hand-edit vendored Plotly bundles.
|
|
30
|
+
|
|
31
|
+
## Comments and docs
|
|
32
|
+
|
|
33
|
+
- No docstrings except on base/abstract contracts and complex functions that need them.
|
|
34
|
+
- No unnecessary comments; if needed, explain why, not what.
|
|
35
|
+
- No section-divider comments in classes/functions (allowed in test classes). Group by proximity.
|
|
36
|
+
|
|
37
|
+
## Language
|
|
38
|
+
|
|
39
|
+
Shared: ASCII only in identifiers/comments/docs (no emojis). No unnecessary imports.
|
|
40
|
+
|
|
41
|
+
Python: type-hint every function/method; f-strings; join with `/` on `Path` (avoid extra wrappers).
|
|
42
|
+
|
|
43
|
+
## Quality
|
|
44
|
+
|
|
45
|
+
- Deletion over addition. Boring over clever. Fewest files; shortest working diff after understanding the problem.
|
|
46
|
+
- No unrequested abstractions, boilerplate, or dependencies. Question complex asks: does Y cover it?
|
|
47
|
+
- Prefer the edge-case-correct stdlib approach when sizes match.
|
|
48
|
+
- Deliberate ceilings: `ponytail:` comment naming the ceiling and upgrade path.
|
|
49
|
+
- Non-trivial logic: one smallest test that fails if it breaks. Run `poe test` before claiming done.
|
|
50
|
+
- No secrets, `.env`, or local virtualenvs in commits.
|
|
51
|
+
|
|
52
|
+
Not lazy about: understanding the problem, trust-boundary validation, error handling that prevents data loss, security, anything explicitly requested.
|
|
@@ -10,7 +10,8 @@ First-party runtime JS lives under `src/plotly_print/js/`. It runs inside QuickJ
|
|
|
10
10
|
|
|
11
11
|
## Do not edit
|
|
12
12
|
|
|
13
|
-
- `plotly_comprehensive_2d.js` and any
|
|
13
|
+
- `plotly_comprehensive_2d.js` (and any vendored Plotly bundle): opaque; in `.cursorignore`. Do not Read/Grep the whole file.
|
|
14
|
+
- Need a call shape: Shell `rg -n 'pattern' src/plotly_print/js/plotly_comprehensive_2d.js | head` (cap hits). Never open whole-file.
|
|
14
15
|
- Only change first-party files such as `micro_dom.js`.
|
|
15
16
|
|
|
16
17
|
## Style (match `micro_dom.js`)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Package map — start here; avoid re-exploring the tree
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Package map
|
|
7
|
+
|
|
8
|
+
Browser-less Plotly static export: QuickJS + micro-DOM + vendored Plotly 2D → SVG → resvg raster.
|
|
9
|
+
|
|
10
|
+
## Flow
|
|
11
|
+
|
|
12
|
+
`core.to_svg` / `to_image` / `write_image` → `engine.PrintEngine` (QuickJS) → eval `micro_dom.js` + vendored Plotly → SVG → `raster.py` for png/jpeg/webp.
|
|
13
|
+
|
|
14
|
+
`plotly_hook.register` patches Plotly `to_image` / `write_image`.
|
|
15
|
+
|
|
16
|
+
## Files
|
|
17
|
+
|
|
18
|
+
| Path | Job |
|
|
19
|
+
|------|-----|
|
|
20
|
+
| `core.py` | Public API; figure → JSON |
|
|
21
|
+
| `engine.py` | QuickJS lifecycle; canvas→PNG; typed arrays |
|
|
22
|
+
| `fonts.py` | Text metrics callable into JS |
|
|
23
|
+
| `raster.py` | SVG → raster (resvg) |
|
|
24
|
+
| `plotly_hook.py` | Monkeypatch Plotly |
|
|
25
|
+
| `js/micro_dom.js` | First-party DOM/SVG stubs (**edit here**) |
|
|
26
|
+
| `js/plotly_comprehensive_2d.js` | Vendored; **never** Read/Grep whole file (`.cursorignore`) |
|
|
27
|
+
|
|
28
|
+
Start from this map. Open only listed files unless the task is outside `src/plotly_print/`.
|
|
29
|
+
|
|
30
|
+
## Tests / commands / scratch
|
|
31
|
+
|
|
32
|
+
- `tests/test_svg_*.py` → micro-DOM; chart `test_*.py` → render; `test_kaleido_*` → visual parity
|
|
33
|
+
- `poe format` | `poe test` | `poe benchmark` | `poe format_docs`
|
|
34
|
+
- Scratch under `.artifacts/` (gitignored); do not commit
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Agent instructions
|
|
2
|
+
|
|
3
|
+
plotly-print: Python package + QuickJS micro-DOM for browser-less Plotly static export.
|
|
4
|
+
|
|
5
|
+
## Tooling
|
|
6
|
+
|
|
7
|
+
- **All AI IDEs:** start from this file and the package map.
|
|
8
|
+
- **Cursor:** rules in [`.cursor/rules/`](.cursor/rules/). Do not duplicate them here.
|
|
9
|
+
|
|
10
|
+
## Standards (Cursor)
|
|
11
|
+
|
|
12
|
+
- Map (always-on): `.cursor/rules/map.mdc`
|
|
13
|
+
- Coding standards (always-on): `.cursor/rules/global.mdc`
|
|
14
|
+
- JavaScript (`src/plotly_print/js/`): `.cursor/rules/javascript.mdc`
|
|
15
|
+
- Markdown (`.md`): `.cursor/rules/markdown.mdc`
|
|
16
|
+
|
|
17
|
+
Vendored Plotly bundle is listed in `.cursorignore` — do not open it whole-file.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: plotly-print
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Lightweight, browser-less static image exporter for Plotly
|
|
5
5
|
Project-URL: Homepage, https://github.com/roman-kaczanowski/plotly-print
|
|
6
6
|
Project-URL: Repository, https://github.com/roman-kaczanowski/plotly-print
|
|
@@ -4,6 +4,7 @@ import base64
|
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
+
import struct
|
|
7
8
|
import threading
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
@@ -69,6 +70,69 @@ def _encode_canvas_to_png(width: int, height: int, commands_json: str) -> str:
|
|
|
69
70
|
return f'data:image/png;base64,{b64}'
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
# QuickJS throws on the out-of-bounds Uint8Array write plotly.js uses to drop
|
|
74
|
+
# base64 padding when decoding {'dtype', 'bdata'}. Expand those specs to lists
|
|
75
|
+
# before they reach the engine. Bytes are little-endian, matching the typed
|
|
76
|
+
# arrays plotly.js builds on the hosts this package runs on.
|
|
77
|
+
_TYPED_ARRAY_DTYPES = {
|
|
78
|
+
'i1': 'b',
|
|
79
|
+
'u1': 'B',
|
|
80
|
+
'i2': 'h',
|
|
81
|
+
'u2': 'H',
|
|
82
|
+
'i4': 'i',
|
|
83
|
+
'u4': 'I',
|
|
84
|
+
'f4': 'f',
|
|
85
|
+
'f8': 'd',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _decode_typed_array(spec: dict[str, Any]) -> list[Any] | None:
|
|
90
|
+
dtype = spec.get('dtype')
|
|
91
|
+
bdata = spec.get('bdata')
|
|
92
|
+
if not isinstance(dtype, str) or not isinstance(bdata, str):
|
|
93
|
+
return None
|
|
94
|
+
code = _TYPED_ARRAY_DTYPES.get(dtype)
|
|
95
|
+
if code is None:
|
|
96
|
+
return None
|
|
97
|
+
shape = spec.get('shape')
|
|
98
|
+
if 'shape' in spec and not isinstance(shape, (str, int)):
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
raw = base64.b64decode(bdata)
|
|
102
|
+
width = struct.calcsize(code)
|
|
103
|
+
if len(raw) % width:
|
|
104
|
+
raise ValueError(f'typed array byte length {len(raw)} is not a multiple of {width} for dtype {dtype}')
|
|
105
|
+
flat = list(struct.unpack(f'<{len(raw) // width}{code}', raw))
|
|
106
|
+
if shape is None:
|
|
107
|
+
return flat
|
|
108
|
+
dims = [int(part) for part in str(shape).split(',') if part.strip()]
|
|
109
|
+
return _reshape_row_major(flat, dims)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _reshape_row_major(flat: list[Any], dims: list[int]) -> list[Any]:
|
|
113
|
+
size = 1
|
|
114
|
+
for dim in dims:
|
|
115
|
+
size *= dim
|
|
116
|
+
if size != len(flat):
|
|
117
|
+
joined = ','.join(str(dim) for dim in dims)
|
|
118
|
+
raise ValueError(f'typed array shape {joined} does not match {len(flat)} values')
|
|
119
|
+
if len(dims) <= 1:
|
|
120
|
+
return flat
|
|
121
|
+
span = size // dims[0]
|
|
122
|
+
return [_reshape_row_major(flat[index * span : (index + 1) * span], dims[1:]) for index in range(dims[0])]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _expand_typed_arrays(value: Any) -> Any:
|
|
126
|
+
if isinstance(value, dict):
|
|
127
|
+
decoded = _decode_typed_array(value)
|
|
128
|
+
if decoded is not None:
|
|
129
|
+
return decoded
|
|
130
|
+
return {key: _expand_typed_arrays(item) for key, item in value.items()}
|
|
131
|
+
if isinstance(value, list):
|
|
132
|
+
return [_expand_typed_arrays(item) for item in value]
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
72
136
|
class PrintEngine:
|
|
73
137
|
def __init__(self) -> None:
|
|
74
138
|
self.font_manager = get_default_font_manager()
|
|
@@ -155,8 +219,8 @@ class PrintEngine:
|
|
|
155
219
|
if height is not None:
|
|
156
220
|
layout_copy['height'] = height
|
|
157
221
|
|
|
158
|
-
data_json = json.dumps(data, cls=PlotlyJSONEncoder)
|
|
159
|
-
layout_json = json.dumps(layout_copy, cls=PlotlyJSONEncoder)
|
|
222
|
+
data_json = json.dumps(_expand_typed_arrays(data), cls=PlotlyJSONEncoder)
|
|
223
|
+
layout_json = json.dumps(_expand_typed_arrays(layout_copy), cls=PlotlyJSONEncoder)
|
|
160
224
|
|
|
161
225
|
self.context.eval(f'var __plotly_print_data = {json.dumps(data_json)};')
|
|
162
226
|
self.context.eval(f'var __plotly_print_layout = {json.dumps(layout_json)};')
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
4
|
import decimal
|
|
5
|
+
import json
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -9,6 +10,7 @@ import plotly.graph_objects as go
|
|
|
9
10
|
import pytest
|
|
10
11
|
|
|
11
12
|
import plotly_print
|
|
13
|
+
from plotly_print.engine import _expand_typed_arrays
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@pytest.mark.parametrize(
|
|
@@ -135,6 +137,62 @@ def test_non_json_values_render(name, fig):
|
|
|
135
137
|
assert png.startswith(b'\x89PNG')
|
|
136
138
|
|
|
137
139
|
|
|
140
|
+
def test_typed_array_specs_expand_to_plain_values():
|
|
141
|
+
x = np.array([1.5, -2.25], dtype=np.float64)
|
|
142
|
+
y = np.array([-1, 2, 3, 4], dtype=np.int8)
|
|
143
|
+
color = np.array([0.25, 0.5], dtype=np.float32)
|
|
144
|
+
size = np.array([10, 20], dtype=np.uint16)
|
|
145
|
+
z = np.arange(4, dtype=np.float64).reshape(2, 2)
|
|
146
|
+
fig = go.Figure(
|
|
147
|
+
data=[
|
|
148
|
+
go.Scatter(x=x, y=y, marker={'color': color, 'size': size}),
|
|
149
|
+
go.Heatmap(z=z),
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
expanded = _expand_typed_arrays(fig.to_dict())
|
|
154
|
+
|
|
155
|
+
assert expanded['data'][0]['x'] == x.tolist()
|
|
156
|
+
assert expanded['data'][0]['y'] == y.tolist()
|
|
157
|
+
assert expanded['data'][0]['marker']['color'] == color.tolist()
|
|
158
|
+
assert expanded['data'][0]['marker']['size'] == size.tolist()
|
|
159
|
+
assert expanded['data'][1]['z'] == z.tolist()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize('source', ['figure', 'to_dict', 'json'])
|
|
163
|
+
def test_padded_numpy_bdata_renders(source):
|
|
164
|
+
# 22 float64 values are 176 bytes, which is not a multiple of 3, so the
|
|
165
|
+
# base64 payload has padding that used to crash QuickJS.
|
|
166
|
+
values = np.arange(22, dtype=float)
|
|
167
|
+
fig = go.Figure(
|
|
168
|
+
data=[go.Scatter(x=values, y=values, mode='lines+markers')],
|
|
169
|
+
layout=go.Layout(width=400, height=300, title='Bdata'),
|
|
170
|
+
)
|
|
171
|
+
if source == 'to_dict':
|
|
172
|
+
fig = fig.to_dict()
|
|
173
|
+
elif source == 'json':
|
|
174
|
+
fig = json.loads(fig.to_json())
|
|
175
|
+
|
|
176
|
+
svg = plotly_print.to_svg(fig)
|
|
177
|
+
assert '<svg' in svg
|
|
178
|
+
assert 'Bdata' in svg
|
|
179
|
+
assert 'class="point"' in svg
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_padded_pandas_and_heatmap_render():
|
|
183
|
+
series = pd.Series(np.arange(22, dtype=float))
|
|
184
|
+
figures = [
|
|
185
|
+
go.Figure(data=[go.Scatter(x=series, y=series**2)], layout=go.Layout(width=400, height=300)),
|
|
186
|
+
go.Figure(
|
|
187
|
+
data=[go.Heatmap(z=np.arange(4, dtype=float).reshape(2, 2))], layout=go.Layout(width=400, height=300)
|
|
188
|
+
),
|
|
189
|
+
go.Figure(data=[go.Bar(y=np.array([1, 2], dtype=np.int16))], layout=go.Layout(width=400, height=300)),
|
|
190
|
+
]
|
|
191
|
+
for fig in figures:
|
|
192
|
+
svg = plotly_print.to_svg(fig)
|
|
193
|
+
assert '<svg' in svg
|
|
194
|
+
|
|
195
|
+
|
|
138
196
|
def test_hooked_to_image_with_decimal_values():
|
|
139
197
|
plotly_print.register()
|
|
140
198
|
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Gitignored local output directory for agents
|
|
3
|
-
alwaysApply: true
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# `.artifacts/`
|
|
7
|
-
|
|
8
|
-
The repo root `.artifacts/` directory is **gitignored**. Use it for:
|
|
9
|
-
|
|
10
|
-
- LLM research notes, comparison reports, scratch exports, and other **temporary local artifacts**
|
|
11
|
-
|
|
12
|
-
Do not commit files under `.artifacts/`. Nothing there is shipped on PyPI or in the wheel.
|
|
13
|
-
|
|
14
|
-
When generating research or large binary outputs for the user to review (outside the benchmark), prefer paths under `.artifacts/` unless they ask for another location.
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Coding standards for plotly-print (Python package + QuickJS micro-DOM)
|
|
3
|
-
alwaysApply: true
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Project standards
|
|
7
|
-
|
|
8
|
-
plotly-print is a Python package (cookiecutter-python-seed layout) plus first-party QuickJS JavaScript under `src/plotly_print/js/` (micro-DOM for static Plotly rendering). Treat `pyproject.toml` as the source of truth for toolchain, Python version, and dependencies. Minimum Python version is 3.12. Use uv to lock, sync, build, and bump versions.
|
|
9
|
-
|
|
10
|
-
JS-specific conventions: `.cursor/rules/javascript.mdc` (applies under `src/plotly_print/js/`).
|
|
11
|
-
|
|
12
|
-
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
|
13
|
-
|
|
14
|
-
## Before writing code
|
|
15
|
-
|
|
16
|
-
Read the task and the code it touches, trace the real flow end to end, then stop at the first rung that holds:
|
|
17
|
-
|
|
18
|
-
1. Does this need to be built at all? (YAGNI)
|
|
19
|
-
2. Does it already exist in this codebase? Reuse it.
|
|
20
|
-
3. Does the standard library already do this? Use it.
|
|
21
|
-
4. Does an already-declared dependency solve it? Use it.
|
|
22
|
-
5. Can this be one boring line? Make it one line.
|
|
23
|
-
6. Only then: write the minimum code that works.
|
|
24
|
-
|
|
25
|
-
Bug fix = root cause, not symptom. Grep every caller of the function you touch and fix the shared function once.
|
|
26
|
-
|
|
27
|
-
## Toolchain
|
|
28
|
-
|
|
29
|
-
- Match existing layout: `src/` for packages, `tests/` for tests, application entry points as already configured.
|
|
30
|
-
- Do not add a dependency unless it is declared in `pyproject.toml` (or the project's lockfile). Prefer stdlib over a new package.
|
|
31
|
-
- Python style is enforced by Ruff. Follow `pyproject.toml`; the points below are the ones to apply consistently even when Ruff cannot.
|
|
32
|
-
- After editing Python files, run `poe format`.
|
|
33
|
-
- After editing Markdown files, run `poe format_docs`.
|
|
34
|
-
- There is no JS formatter/linter task; match `micro_dom.js` style. Do not hand-edit vendored Plotly bundles.
|
|
35
|
-
|
|
36
|
-
## Comments and docs
|
|
37
|
-
|
|
38
|
-
- No docstrings except on base/abstract classes and methods (the contract for implementors) and on complex functions where extra explanation genuinely helps.
|
|
39
|
-
- No unnecessary comments. Code should be self-explanatory. If a comment is needed, explain why, not what.
|
|
40
|
-
- No section-divider comments inside classes or functions. Group related methods by proximity. Dividers are allowed only in test classes.
|
|
41
|
-
|
|
42
|
-
## Language
|
|
43
|
-
|
|
44
|
-
Shared (Python and JS):
|
|
45
|
-
|
|
46
|
-
- ASCII only in identifiers, comments, and docs. No emojis. Non-ASCII letters are allowed only when the content is actually in another language (e.g. Polish, Russian).
|
|
47
|
-
- No unnecessary imports.
|
|
48
|
-
|
|
49
|
-
Python:
|
|
50
|
-
|
|
51
|
-
- Type-hint every function and method. Do not nest annotations so deeply that they hurt readability.
|
|
52
|
-
- Use f-strings for all string formatting.
|
|
53
|
-
- Avoid unnecessary `Path` wrappers; join with `/` on `Path` objects.
|
|
54
|
-
|
|
55
|
-
## Quality
|
|
56
|
-
|
|
57
|
-
- Use `poe format` / `poe format_docs` instead of hand-formatting style (Python/Markdown).
|
|
58
|
-
- Deletion over addition. Boring over clever. Fewest files possible. Shortest working diff wins, after you understand the problem.
|
|
59
|
-
- No abstractions, boilerplate, or dependencies that weren't requested.
|
|
60
|
-
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
|
61
|
-
- When two stdlib approaches are the same size, pick the edge-case-correct one.
|
|
62
|
-
- Mark deliberate simplifications that cut a real corner with a known ceiling (global lock, O(n^2) scan, naive heuristic) with a `ponytail:` comment naming the ceiling and upgrade path.
|
|
63
|
-
- Non-trivial logic leaves one runnable check: the smallest test that fails if the logic breaks. Trivial one-liners need no new test. Run the project's test command before claiming work is done.
|
|
64
|
-
- Do not commit secrets, `.env` files, or local virtualenvs.
|
|
65
|
-
|
|
66
|
-
Not lazy about: understanding the problem, input validation at trust boundaries, error handling that prevents data loss, security, anything explicitly requested.
|
|
67
|
-
|
|
68
|
-
## Agents
|
|
69
|
-
|
|
70
|
-
- Keep this file for conventions that should always apply.
|
|
71
|
-
- Put path-specific Cursor rules in additional `.mdc` files in this directory.
|
|
72
|
-
- Keep `AGENTS.md` as a short index that points here.
|
plotly_print-0.2.2/AGENTS.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Agent instructions
|
|
2
|
-
|
|
3
|
-
This project was generated from **cookiecutter-python-seed**, with first-party QuickJS JavaScript for Plotly static rendering.
|
|
4
|
-
|
|
5
|
-
## Tooling
|
|
6
|
-
|
|
7
|
-
- **All AI IDEs:** follow this file.
|
|
8
|
-
- **Cursor:** project rules live in [`.cursor/rules/`](.cursor/rules/). This file is the index; do not duplicate those rules here.
|
|
9
|
-
|
|
10
|
-
## Standards
|
|
11
|
-
|
|
12
|
-
- Always-on: `.cursor/rules/global.mdc`
|
|
13
|
-
- JavaScript (`src/plotly_print/js/`): `.cursor/rules/javascript.mdc`
|
|
14
|
-
- Markdown (`.md` files): `.cursor/rules/markdown.mdc`
|
|
15
|
-
- Local scratch output: `.cursor/rules/artifacts.mdc`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_annotations_arrowheads.png
RENAMED
|
File without changes
|
{plotly_print-0.2.2 → plotly_print-0.2.3}/assets/benchmarks/compare_annotations_long_arrows.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|