licos-dev-sdk 0.3.2__tar.gz → 0.3.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.
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/PKG-INFO +1 -1
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/pyproject.toml +1 -1
- licos_dev_sdk-0.3.3/src/licos_dev_sdk/chart.py +114 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_document_spreadsheet.py +28 -1
- licos_dev_sdk-0.3.2/src/licos_dev_sdk/chart.py +0 -80
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/.gitignore +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/model.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_model.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_output_paths.py +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Chart generation — bar, line, pie, scatter, heatmap via matplotlib."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import matplotlib
|
|
8
|
+
matplotlib.use("Agg") # Non-interactive backend
|
|
9
|
+
from matplotlib import font_manager
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ._utils import resolve_output_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_CJK_FONT_CANDIDATES = (
|
|
17
|
+
"Noto Sans CJK SC",
|
|
18
|
+
"Noto Sans SC",
|
|
19
|
+
"Microsoft YaHei",
|
|
20
|
+
"SimHei",
|
|
21
|
+
"WenQuanYi Zen Hei",
|
|
22
|
+
"Source Han Sans CN",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=1)
|
|
27
|
+
def _resolve_cjk_font() -> str:
|
|
28
|
+
installed_fonts = {font.name.casefold(): font.name for font in font_manager.fontManager.ttflist}
|
|
29
|
+
for candidate in _CJK_FONT_CANDIDATES:
|
|
30
|
+
installed_name = installed_fonts.get(candidate.casefold())
|
|
31
|
+
if installed_name:
|
|
32
|
+
return installed_name
|
|
33
|
+
choices = ", ".join(_CJK_FONT_CANDIDATES)
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
"Chinese chart font is unavailable. Install a CJK font before generating charts "
|
|
36
|
+
f"(supported families: {choices})."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_chart(chart_type: str, data: dict, filename: str, *,
|
|
41
|
+
output_dir: str | None = None, format: str = "png",
|
|
42
|
+
title: str = "", width: int = 800, height: int = 600) -> str:
|
|
43
|
+
"""Generate a chart image (PNG/SVG). Returns absolute path.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
chart_type: "bar" | "line" | "pie" | "scatter" | "heatmap"
|
|
47
|
+
data: {
|
|
48
|
+
"labels": ["A", "B", "C"],
|
|
49
|
+
"datasets": [{"label": "Series 1", "values": [10, 20, 30]}]
|
|
50
|
+
}
|
|
51
|
+
For pie: only first dataset is used.
|
|
52
|
+
For heatmap: data["matrix"] = [[1,2],[3,4]], data["x_labels"], data["y_labels"]
|
|
53
|
+
"""
|
|
54
|
+
path = resolve_output_path(filename, f".{format}", output_dir)
|
|
55
|
+
dpi = 100
|
|
56
|
+
font_name = _resolve_cjk_font()
|
|
57
|
+
chart_style = {
|
|
58
|
+
"font.family": "sans-serif",
|
|
59
|
+
"font.sans-serif": [font_name],
|
|
60
|
+
"axes.unicode_minus": False,
|
|
61
|
+
}
|
|
62
|
+
with matplotlib.rc_context(chart_style):
|
|
63
|
+
fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
|
|
64
|
+
|
|
65
|
+
labels = data.get("labels", [])
|
|
66
|
+
datasets = data.get("datasets", [])
|
|
67
|
+
|
|
68
|
+
if chart_type == "bar":
|
|
69
|
+
x = np.arange(len(labels))
|
|
70
|
+
n = len(datasets)
|
|
71
|
+
bar_width = 0.8 / max(n, 1)
|
|
72
|
+
for i, ds in enumerate(datasets):
|
|
73
|
+
offset = (i - n / 2 + 0.5) * bar_width
|
|
74
|
+
ax.bar(x + offset, ds["values"], bar_width, label=ds.get("label", ""))
|
|
75
|
+
ax.set_xticks(x)
|
|
76
|
+
ax.set_xticklabels(labels)
|
|
77
|
+
if n > 1:
|
|
78
|
+
ax.legend()
|
|
79
|
+
|
|
80
|
+
elif chart_type == "line":
|
|
81
|
+
for ds in datasets:
|
|
82
|
+
ax.plot(labels, ds["values"], marker="o", label=ds.get("label", ""))
|
|
83
|
+
if len(datasets) > 1:
|
|
84
|
+
ax.legend()
|
|
85
|
+
|
|
86
|
+
elif chart_type == "pie":
|
|
87
|
+
values = datasets[0]["values"] if datasets else []
|
|
88
|
+
ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
|
|
89
|
+
ax.axis("equal")
|
|
90
|
+
|
|
91
|
+
elif chart_type == "scatter":
|
|
92
|
+
for ds in datasets:
|
|
93
|
+
x_vals = ds.get("x", list(range(len(ds["values"]))))
|
|
94
|
+
ax.scatter(x_vals, ds["values"], label=ds.get("label", ""), alpha=0.7)
|
|
95
|
+
if len(datasets) > 1:
|
|
96
|
+
ax.legend()
|
|
97
|
+
|
|
98
|
+
elif chart_type == "heatmap":
|
|
99
|
+
matrix = np.array(data.get("matrix", [[]]))
|
|
100
|
+
im = ax.imshow(matrix, cmap="YlOrRd", aspect="auto")
|
|
101
|
+
fig.colorbar(im, ax=ax)
|
|
102
|
+
if "x_labels" in data:
|
|
103
|
+
ax.set_xticks(range(len(data["x_labels"])))
|
|
104
|
+
ax.set_xticklabels(data["x_labels"])
|
|
105
|
+
if "y_labels" in data:
|
|
106
|
+
ax.set_yticks(range(len(data["y_labels"])))
|
|
107
|
+
ax.set_yticklabels(data["y_labels"])
|
|
108
|
+
|
|
109
|
+
if title:
|
|
110
|
+
ax.set_title(title)
|
|
111
|
+
fig.tight_layout()
|
|
112
|
+
fig.savefig(str(path), format=format, bbox_inches="tight")
|
|
113
|
+
plt.close(fig)
|
|
114
|
+
return str(path.resolve())
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import sys
|
|
4
4
|
import tempfile
|
|
5
5
|
import unittest
|
|
6
|
+
import warnings
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
@@ -11,10 +12,36 @@ from docx import Document
|
|
|
11
12
|
from openpyxl import load_workbook
|
|
12
13
|
from PIL import Image
|
|
13
14
|
|
|
14
|
-
from licos_dev_sdk import create_docx, create_docx_from_template, create_xlsx_workbook
|
|
15
|
+
from licos_dev_sdk import create_chart, create_docx, create_docx_from_template, create_xlsx_workbook
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class DocumentSpreadsheetTests(unittest.TestCase):
|
|
19
|
+
def test_create_chart_renders_chinese_text_without_missing_glyphs(self) -> None:
|
|
20
|
+
with tempfile.TemporaryDirectory() as tmp, warnings.catch_warnings(record=True) as caught:
|
|
21
|
+
warnings.simplefilter("always")
|
|
22
|
+
output = create_chart(
|
|
23
|
+
"bar",
|
|
24
|
+
{
|
|
25
|
+
"labels": ["华东地区", "华中地区", "华南地区"],
|
|
26
|
+
"datasets": [
|
|
27
|
+
{"label": "预计收入(万元)", "values": [4800, 3200, 2600]},
|
|
28
|
+
{"label": "项目ROI(%)", "values": [18, 23, 16]},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
"chinese-chart",
|
|
32
|
+
output_dir=tmp,
|
|
33
|
+
title="重点行业客户投资回报分析",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.assertTrue(Path(output).is_file())
|
|
37
|
+
self.assertGreater(Path(output).stat().st_size, 10_000)
|
|
38
|
+
missing_glyph_warnings = [
|
|
39
|
+
str(item.message)
|
|
40
|
+
for item in caught
|
|
41
|
+
if "Glyph" in str(item.message) and "missing from font" in str(item.message)
|
|
42
|
+
]
|
|
43
|
+
self.assertEqual(missing_glyph_warnings, [])
|
|
44
|
+
|
|
18
45
|
def test_create_docx_resolves_assets_from_explicit_base_path(self) -> None:
|
|
19
46
|
with tempfile.TemporaryDirectory() as tmp:
|
|
20
47
|
tmp_path = Path(tmp)
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""Chart generation — bar, line, pie, scatter, heatmap via matplotlib."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import matplotlib
|
|
6
|
-
matplotlib.use("Agg") # Non-interactive backend
|
|
7
|
-
import matplotlib.pyplot as plt
|
|
8
|
-
import numpy as np
|
|
9
|
-
|
|
10
|
-
from ._utils import resolve_output_path
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def create_chart(chart_type: str, data: dict, filename: str, *,
|
|
14
|
-
output_dir: str | None = None, format: str = "png",
|
|
15
|
-
title: str = "", width: int = 800, height: int = 600) -> str:
|
|
16
|
-
"""Generate a chart image (PNG/SVG). Returns absolute path.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
chart_type: "bar" | "line" | "pie" | "scatter" | "heatmap"
|
|
20
|
-
data: {
|
|
21
|
-
"labels": ["A", "B", "C"],
|
|
22
|
-
"datasets": [{"label": "Series 1", "values": [10, 20, 30]}]
|
|
23
|
-
}
|
|
24
|
-
For pie: only first dataset is used.
|
|
25
|
-
For heatmap: data["matrix"] = [[1,2],[3,4]], data["x_labels"], data["y_labels"]
|
|
26
|
-
"""
|
|
27
|
-
path = resolve_output_path(filename, f".{format}", output_dir)
|
|
28
|
-
dpi = 100
|
|
29
|
-
fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
|
|
30
|
-
|
|
31
|
-
labels = data.get("labels", [])
|
|
32
|
-
datasets = data.get("datasets", [])
|
|
33
|
-
|
|
34
|
-
if chart_type == "bar":
|
|
35
|
-
x = np.arange(len(labels))
|
|
36
|
-
n = len(datasets)
|
|
37
|
-
bar_width = 0.8 / max(n, 1)
|
|
38
|
-
for i, ds in enumerate(datasets):
|
|
39
|
-
offset = (i - n / 2 + 0.5) * bar_width
|
|
40
|
-
ax.bar(x + offset, ds["values"], bar_width, label=ds.get("label", ""))
|
|
41
|
-
ax.set_xticks(x)
|
|
42
|
-
ax.set_xticklabels(labels)
|
|
43
|
-
if n > 1:
|
|
44
|
-
ax.legend()
|
|
45
|
-
|
|
46
|
-
elif chart_type == "line":
|
|
47
|
-
for ds in datasets:
|
|
48
|
-
ax.plot(labels, ds["values"], marker="o", label=ds.get("label", ""))
|
|
49
|
-
if len(datasets) > 1:
|
|
50
|
-
ax.legend()
|
|
51
|
-
|
|
52
|
-
elif chart_type == "pie":
|
|
53
|
-
values = datasets[0]["values"] if datasets else []
|
|
54
|
-
ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
|
|
55
|
-
ax.axis("equal")
|
|
56
|
-
|
|
57
|
-
elif chart_type == "scatter":
|
|
58
|
-
for ds in datasets:
|
|
59
|
-
x_vals = ds.get("x", list(range(len(ds["values"]))))
|
|
60
|
-
ax.scatter(x_vals, ds["values"], label=ds.get("label", ""), alpha=0.7)
|
|
61
|
-
if len(datasets) > 1:
|
|
62
|
-
ax.legend()
|
|
63
|
-
|
|
64
|
-
elif chart_type == "heatmap":
|
|
65
|
-
matrix = np.array(data.get("matrix", [[]]))
|
|
66
|
-
im = ax.imshow(matrix, cmap="YlOrRd", aspect="auto")
|
|
67
|
-
fig.colorbar(im, ax=ax)
|
|
68
|
-
if "x_labels" in data:
|
|
69
|
-
ax.set_xticks(range(len(data["x_labels"])))
|
|
70
|
-
ax.set_xticklabels(data["x_labels"])
|
|
71
|
-
if "y_labels" in data:
|
|
72
|
-
ax.set_yticks(range(len(data["y_labels"])))
|
|
73
|
-
ax.set_yticklabels(data["y_labels"])
|
|
74
|
-
|
|
75
|
-
if title:
|
|
76
|
-
ax.set_title(title)
|
|
77
|
-
fig.tight_layout()
|
|
78
|
-
fig.savefig(str(path), format=format, bbox_inches="tight")
|
|
79
|
-
plt.close(fig)
|
|
80
|
-
return str(path.resolve())
|
|
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
|