plotly-print 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.
- plotly_print/__init__.py +20 -0
- plotly_print/core.py +76 -0
- plotly_print/engine.py +172 -0
- plotly_print/fonts.py +143 -0
- plotly_print/js/micro_dom.js +1195 -0
- plotly_print/js/plotly-cartesian.min.js +149 -0
- plotly_print/plotly_hook.py +95 -0
- plotly_print/raster.py +186 -0
- plotly_print-0.1.0.dist-info/METADATA +66 -0
- plotly_print-0.1.0.dist-info/RECORD +13 -0
- plotly_print-0.1.0.dist-info/WHEEL +4 -0
- plotly_print-0.1.0.dist-info/licenses/LICENSE +22 -0
- plotly_print-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +40 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, BinaryIO
|
|
4
|
+
|
|
5
|
+
import plotly_print.core as core
|
|
6
|
+
|
|
7
|
+
_ORIGINAL_TO_IMAGE = None
|
|
8
|
+
_ORIGINAL_WRITE_IMAGE = None
|
|
9
|
+
_IS_PATCHED = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _patched_to_image(
|
|
13
|
+
fig: Any,
|
|
14
|
+
format: str | None = None,
|
|
15
|
+
width: int | None = None,
|
|
16
|
+
height: int | None = None,
|
|
17
|
+
scale: float | None = None,
|
|
18
|
+
validate: bool = True,
|
|
19
|
+
**kwargs: Any,
|
|
20
|
+
) -> bytes:
|
|
21
|
+
return core.to_image(
|
|
22
|
+
fig,
|
|
23
|
+
format=format or 'png',
|
|
24
|
+
width=width,
|
|
25
|
+
height=height,
|
|
26
|
+
scale=scale or 1.0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _patched_write_image(
|
|
31
|
+
fig: Any,
|
|
32
|
+
file: str | BinaryIO,
|
|
33
|
+
format: str | None = None,
|
|
34
|
+
scale: float | None = None,
|
|
35
|
+
width: int | None = None,
|
|
36
|
+
height: int | None = None,
|
|
37
|
+
validate: bool = True,
|
|
38
|
+
**kwargs: Any,
|
|
39
|
+
) -> None:
|
|
40
|
+
core.write_image(
|
|
41
|
+
fig,
|
|
42
|
+
file=file,
|
|
43
|
+
format=format,
|
|
44
|
+
scale=scale or 1.0,
|
|
45
|
+
width=width,
|
|
46
|
+
height=height,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register() -> None:
|
|
51
|
+
global _ORIGINAL_TO_IMAGE, _ORIGINAL_WRITE_IMAGE, _IS_PATCHED
|
|
52
|
+
if _IS_PATCHED:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
import plotly.graph_objects as go
|
|
57
|
+
import plotly.io as pio
|
|
58
|
+
|
|
59
|
+
_ORIGINAL_TO_IMAGE = pio.to_image
|
|
60
|
+
_ORIGINAL_WRITE_IMAGE = pio.write_image
|
|
61
|
+
|
|
62
|
+
pio.to_image = _patched_to_image
|
|
63
|
+
pio.write_image = _patched_write_image
|
|
64
|
+
go.Figure.to_image = _patched_to_image
|
|
65
|
+
go.Figure.write_image = _patched_write_image
|
|
66
|
+
|
|
67
|
+
_IS_PATCHED = True
|
|
68
|
+
except ImportError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def unregister() -> None:
|
|
73
|
+
global _ORIGINAL_TO_IMAGE, _ORIGINAL_WRITE_IMAGE, _IS_PATCHED
|
|
74
|
+
if not _IS_PATCHED:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
import plotly.graph_objects as go
|
|
79
|
+
import plotly.io as pio
|
|
80
|
+
|
|
81
|
+
if _ORIGINAL_TO_IMAGE:
|
|
82
|
+
pio.to_image = _ORIGINAL_TO_IMAGE
|
|
83
|
+
go.Figure.to_image = _ORIGINAL_TO_IMAGE
|
|
84
|
+
|
|
85
|
+
if _ORIGINAL_WRITE_IMAGE:
|
|
86
|
+
pio.write_image = _ORIGINAL_WRITE_IMAGE
|
|
87
|
+
go.Figure.write_image = _ORIGINAL_WRITE_IMAGE
|
|
88
|
+
|
|
89
|
+
_IS_PATCHED = False
|
|
90
|
+
except ImportError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
patch_plotly = register
|
|
95
|
+
unpatch_plotly = unregister
|
plotly_print/raster.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import BinaryIO
|
|
7
|
+
|
|
8
|
+
import resvg_py
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
SYSTEM_FONT_DIRS = [
|
|
12
|
+
'/System/Library/Fonts/Supplemental',
|
|
13
|
+
'/Library/Fonts',
|
|
14
|
+
'/System/Library/Fonts',
|
|
15
|
+
'/usr/share/fonts',
|
|
16
|
+
'/usr/local/share/fonts',
|
|
17
|
+
]
|
|
18
|
+
EXISTING_FONT_DIRS = [d for d in SYSTEM_FONT_DIRS if Path(d).is_dir()]
|
|
19
|
+
|
|
20
|
+
COMMON_FONT_NAMES = [
|
|
21
|
+
'verdana',
|
|
22
|
+
'arial',
|
|
23
|
+
'tahoma',
|
|
24
|
+
'trebuchet ms',
|
|
25
|
+
'helvetica',
|
|
26
|
+
'times new roman',
|
|
27
|
+
'courier new',
|
|
28
|
+
'georgia',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_SVG_ROOT_RE = re.compile(r'<svg\b[^>]*>', re.IGNORECASE)
|
|
32
|
+
_ATTR_RE = re.compile(r'([^\s=]+)\s*=\s*("([^"]*)"|\'([^\']*)\')')
|
|
33
|
+
_LENGTH_RE = re.compile(r'^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*[a-z%]*$', re.IGNORECASE)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalize_svg_fonts(svg: str) -> str:
|
|
37
|
+
def _cap(m: re.Match[str]) -> str:
|
|
38
|
+
val = m.group(0)
|
|
39
|
+
for name in COMMON_FONT_NAMES:
|
|
40
|
+
val = re.sub(r'\b' + re.escape(name) + r'\b', name.title(), val, flags=re.IGNORECASE)
|
|
41
|
+
return val
|
|
42
|
+
|
|
43
|
+
return re.sub(r'font-family:\s*[^;\"}]+', _cap, svg)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_length(value: str | None) -> float | None:
|
|
47
|
+
if value is None:
|
|
48
|
+
return None
|
|
49
|
+
m = _LENGTH_RE.match(value.strip())
|
|
50
|
+
if not m:
|
|
51
|
+
return None
|
|
52
|
+
return float(m.group(1))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_svg_layout_size(svg: str) -> tuple[float, float]:
|
|
56
|
+
root = _SVG_ROOT_RE.search(svg)
|
|
57
|
+
if not root:
|
|
58
|
+
raise ValueError('SVG root element not found')
|
|
59
|
+
|
|
60
|
+
attrs = {
|
|
61
|
+
m.group(1).lower(): (m.group(3) if m.group(3) is not None else m.group(4))
|
|
62
|
+
for m in _ATTR_RE.finditer(root.group(0))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
width = _parse_length(attrs.get('width'))
|
|
66
|
+
height = _parse_length(attrs.get('height'))
|
|
67
|
+
|
|
68
|
+
if (width is None or height is None) and 'viewbox' in attrs:
|
|
69
|
+
parts = attrs['viewbox'].replace(',', ' ').split()
|
|
70
|
+
if len(parts) == 4:
|
|
71
|
+
if width is None:
|
|
72
|
+
width = float(parts[2])
|
|
73
|
+
if height is None:
|
|
74
|
+
height = float(parts[3])
|
|
75
|
+
|
|
76
|
+
if width is None or height is None:
|
|
77
|
+
raise ValueError('Could not determine SVG layout width/height')
|
|
78
|
+
return width, height
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _prepare_svg_for_raster(
|
|
82
|
+
svg: str,
|
|
83
|
+
width: int | None = None,
|
|
84
|
+
height: int | None = None,
|
|
85
|
+
scale: float = 1.0,
|
|
86
|
+
) -> str:
|
|
87
|
+
layout_w, layout_h = _parse_svg_layout_size(svg)
|
|
88
|
+
if width is not None:
|
|
89
|
+
layout_w = float(width)
|
|
90
|
+
if height is not None:
|
|
91
|
+
layout_h = float(height)
|
|
92
|
+
|
|
93
|
+
out_w = max(1, int(round(layout_w * scale)))
|
|
94
|
+
out_h = max(1, int(round(layout_h * scale)))
|
|
95
|
+
|
|
96
|
+
root = _SVG_ROOT_RE.search(svg)
|
|
97
|
+
if not root:
|
|
98
|
+
raise ValueError('SVG root element not found')
|
|
99
|
+
|
|
100
|
+
tag = root.group(0)
|
|
101
|
+
existing = {m.group(1).lower() for m in _ATTR_RE.finditer(tag)}
|
|
102
|
+
|
|
103
|
+
def _set_attr(tag_str: str, name: str, value: str) -> str:
|
|
104
|
+
pattern = rf'({re.escape(name)}\s*=\s*)("([^"]*)"|\'([^\']*)\')'
|
|
105
|
+
if re.search(pattern, tag_str, flags=re.IGNORECASE):
|
|
106
|
+
return re.sub(pattern, rf'\1"{value}"', tag_str, count=1, flags=re.IGNORECASE)
|
|
107
|
+
return re.sub(r'\s*>$', f' {name}="{value}">', tag_str, count=1)
|
|
108
|
+
|
|
109
|
+
new_tag = tag
|
|
110
|
+
new_tag = _set_attr(new_tag, 'width', str(out_w))
|
|
111
|
+
new_tag = _set_attr(new_tag, 'height', str(out_h))
|
|
112
|
+
if 'viewbox' not in existing:
|
|
113
|
+
vb_w = int(layout_w) if layout_w == int(layout_w) else layout_w
|
|
114
|
+
vb_h = int(layout_h) if layout_h == int(layout_h) else layout_h
|
|
115
|
+
new_tag = _set_attr(new_tag, 'viewBox', f'0 0 {vb_w} {vb_h}')
|
|
116
|
+
|
|
117
|
+
return svg[: root.start()] + new_tag + svg[root.end() :]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def svg_to_png(
|
|
121
|
+
svg: str,
|
|
122
|
+
width: int | None = None,
|
|
123
|
+
height: int | None = None,
|
|
124
|
+
scale: float = 1.0,
|
|
125
|
+
) -> bytes:
|
|
126
|
+
normalized_svg = _normalize_svg_fonts(svg)
|
|
127
|
+
prepared = _prepare_svg_for_raster(normalized_svg, width=width, height=height, scale=scale)
|
|
128
|
+
if EXISTING_FONT_DIRS:
|
|
129
|
+
return resvg_py.svg_to_bytes(prepared, font_dirs=EXISTING_FONT_DIRS)
|
|
130
|
+
return resvg_py.svg_to_bytes(prepared)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def svg_to_jpeg(
|
|
134
|
+
svg: str,
|
|
135
|
+
width: int | None = None,
|
|
136
|
+
height: int | None = None,
|
|
137
|
+
scale: float = 1.0,
|
|
138
|
+
quality: int = 90,
|
|
139
|
+
) -> bytes:
|
|
140
|
+
png_bytes = svg_to_png(svg, width=width, height=height, scale=scale)
|
|
141
|
+
img = Image.open(io.BytesIO(png_bytes))
|
|
142
|
+
if img.mode in ('RGBA', 'LA', 'P'):
|
|
143
|
+
bg = Image.new('RGB', img.size, (255, 255, 255))
|
|
144
|
+
bg.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
|
145
|
+
img = bg
|
|
146
|
+
buf = io.BytesIO()
|
|
147
|
+
img.save(buf, format='JPEG', quality=quality)
|
|
148
|
+
return buf.getvalue()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def svg_to_webp(
|
|
152
|
+
svg: str,
|
|
153
|
+
width: int | None = None,
|
|
154
|
+
height: int | None = None,
|
|
155
|
+
scale: float = 1.0,
|
|
156
|
+
quality: int = 90,
|
|
157
|
+
) -> bytes:
|
|
158
|
+
png_bytes = svg_to_png(svg, width=width, height=height, scale=scale)
|
|
159
|
+
img = Image.open(io.BytesIO(png_bytes))
|
|
160
|
+
buf = io.BytesIO()
|
|
161
|
+
img.save(buf, format='WEBP', quality=quality)
|
|
162
|
+
return buf.getvalue()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def write_raster(
|
|
166
|
+
svg: str,
|
|
167
|
+
target: str | BinaryIO,
|
|
168
|
+
format: str = 'png',
|
|
169
|
+
width: int | None = None,
|
|
170
|
+
height: int | None = None,
|
|
171
|
+
scale: float = 1.0,
|
|
172
|
+
) -> None:
|
|
173
|
+
fmt = format.lower().strip()
|
|
174
|
+
if fmt == 'png':
|
|
175
|
+
data = svg_to_png(svg, width=width, height=height, scale=scale)
|
|
176
|
+
elif fmt in ('jpg', 'jpeg'):
|
|
177
|
+
data = svg_to_jpeg(svg, width=width, height=height, scale=scale)
|
|
178
|
+
elif fmt == 'webp':
|
|
179
|
+
data = svg_to_webp(svg, width=width, height=height, scale=scale)
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError(f"Unsupported raster format: {format}. Supported: 'png', 'jpeg', 'webp'")
|
|
182
|
+
|
|
183
|
+
if isinstance(target, str):
|
|
184
|
+
Path(target).write_bytes(data)
|
|
185
|
+
else:
|
|
186
|
+
target.write(data)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plotly-print
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight, browser-less static image exporter for Plotly
|
|
5
|
+
Project-URL: Homepage, https://github.com/roman-kaczanowski/plotly-print
|
|
6
|
+
Project-URL: Repository, https://github.com/roman-kaczanowski/plotly-print
|
|
7
|
+
Project-URL: Issues, https://github.com/roman-kaczanowski/plotly-print/issues
|
|
8
|
+
Author-email: Roman Kaczanowski <roman.kaczanowski.pl@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
12
|
+
Keywords: export,headless,kaleido,plotly,png,svg,visualization
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: pillow>=9.0.0
|
|
22
|
+
Requires-Dist: plotly>=5.0.0
|
|
23
|
+
Requires-Dist: quickjs>=1.19.0
|
|
24
|
+
Requires-Dist: resvg-py>=0.4.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# plotly-print
|
|
28
|
+
|
|
29
|
+
[](https://github.com/roman-kaczanowski/plotly-print/actions/workflows/quality-checks.yml) [](https://pypi.org/project/plotly-print/) [](https://docs.python.org/3/) [](LICENSE)
|
|
30
|
+
|
|
31
|
+
Lightweight, browser-less static image exporter for Plotly. No Chrome, Chromium, or Playwright. Figures are rendered with an embedded QuickJS runtime, Pillow font metrics, and `resvg-py` for raster output.
|
|
32
|
+
|
|
33
|
+
Works offline: an official plotly.js cartesian partial is bundled (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)).
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install plotly-print
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import plotly.express as px
|
|
45
|
+
import plotly_print
|
|
46
|
+
|
|
47
|
+
fig = px.scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], title='Sales Growth')
|
|
48
|
+
|
|
49
|
+
svg = plotly_print.to_svg(fig)
|
|
50
|
+
png = plotly_print.to_image(fig, format='png', scale=2)
|
|
51
|
+
plotly_print.write_image(fig, 'chart.png')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Patch Plotly's `to_image` / `write_image` to use plotly-print:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import plotly_print
|
|
58
|
+
|
|
59
|
+
plotly_print.register()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and releases.
|
|
63
|
+
|
|
64
|
+
## Attribution
|
|
65
|
+
|
|
66
|
+
plotly-print is an independent project derived from [Mirage](https://github.com/ProfLear/mirage) by Benjamin Lear (MIT). Bundled plotly.js is copyright Plotly, Inc. (MIT); see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
plotly_print/__init__.py,sha256=I1cd1rMBO4T-iXNZHSvXk56feaRVkQZmGD-revYJNX0,472
|
|
2
|
+
plotly_print/core.py,sha256=o7_nxKGJA6pyfst1hnNrcP9OMcpa9oDA0AdEA-LGN3I,2289
|
|
3
|
+
plotly_print/engine.py,sha256=sJuvT9liDvSUWYdyscABr_YqZnJiJh23QpQj3nJ_iC0,6355
|
|
4
|
+
plotly_print/fonts.py,sha256=2EoV2tPeHNxwTtJk4oQsbESwTtnRCmqbfB2qnB5jNsE,5355
|
|
5
|
+
plotly_print/plotly_hook.py,sha256=RGwoGJLH0QOmTEQUtmuE46B1rlDueKCG-vuqwbf-0iI,2121
|
|
6
|
+
plotly_print/raster.py,sha256=K2MfOzjUJrSXwEqAdbZ2pt9jSb_6oSJB74ku3v0GG4c,5642
|
|
7
|
+
plotly_print/js/micro_dom.js,sha256=rcN8N0BQIYukGhTPMsdIzzmXOothJiZQHx5sqTqrhcQ,45471
|
|
8
|
+
plotly_print/js/plotly-cartesian.min.js,sha256=eVQhUIVaUsVnJg4xNwldvit0Tm1wKlyr5n2iRpQvuno,1500041
|
|
9
|
+
plotly_print-0.1.0.dist-info/METADATA,sha256=Yvrn5sAt0Hnhok5z9qcwz2GYkKBLRGYDAZ3Nbz3XFx0,2813
|
|
10
|
+
plotly_print-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
11
|
+
plotly_print-0.1.0.dist-info/licenses/LICENSE,sha256=kKBHCvoTsEA_8Q6ya-9Dh6KkkvdPVcECv16n2V1PtmY,1107
|
|
12
|
+
plotly_print-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=XvdnfmHBOHRcuape6KTVJ45osBjtg4tp5oblhNHst1I,1598
|
|
13
|
+
plotly_print-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Lear
|
|
4
|
+
Copyright (c) 2026 Roman Kaczanowski
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## plotly.js
|
|
4
|
+
|
|
5
|
+
File: `src/plotly_print/js/plotly-cartesian.min.js`
|
|
6
|
+
|
|
7
|
+
Official plotly.js **cartesian** partial (minified) v4.1.1, redistributed for offline /
|
|
8
|
+
isolated environments. The full `plotly.min.js` shipped with the `plotly` Python package
|
|
9
|
+
does not load in the embedded QuickJS runtime, so this smaller official partial is bundled
|
|
10
|
+
instead.
|
|
11
|
+
|
|
12
|
+
Copyright 2012-2026, Plotly, Inc.
|
|
13
|
+
All rights reserved.
|
|
14
|
+
Licensed under the MIT license.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
The MIT License (MIT)
|
|
18
|
+
|
|
19
|
+
Copyright (c) 2014-2026 Plotly, Inc
|
|
20
|
+
|
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
23
|
+
in the Software without restriction, including without limitation the rights
|
|
24
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
25
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
26
|
+
furnished to do so, subject to the following conditions:
|
|
27
|
+
|
|
28
|
+
The above copyright notice and this permission notice shall be included in
|
|
29
|
+
all copies or substantial portions of the Software.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
37
|
+
THE SOFTWARE.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Upstream: https://github.com/plotly/plotly.js
|