pam-python 0.2.12__tar.gz → 0.2.13__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.
- {pam_python-0.2.12/pam_python.egg-info → pam_python-0.2.13}/PKG-INFO +1 -1
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/__init__.py +1 -1
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/cli.py +1 -0
- pam_python-0.2.13/pam/templates/init/REPORTS-th.md +744 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/REPORTS.md +3 -2
- {pam_python-0.2.12 → pam_python-0.2.13/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.2.12 → pam_python-0.2.13}/pam_python.egg-info/SOURCES.txt +1 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_cli_templates.py +8 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/LICENSE.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/README.md +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/api.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/datetime_utils.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/interface_task_manager.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/logger.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/models/__init__.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/models/data_input_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/models/request_command.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/area.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/axis.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/bar.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/box_plot.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/collection.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/definition.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/donut.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/errors.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/fields.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/heatmap.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/line.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/manager.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/metric.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/pie.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/radar.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/range_bar.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/record.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/schema.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/store.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/table.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/temporal.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/transport.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/request_file_format.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/server.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/service.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/sqlite.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/task_manager.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/AGENT.md +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/tester_task.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam/utils.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/pyproject.toml +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/setup.cfg +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_api.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_data_input_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_datetime_utils.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_report_store.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_temporal_mode.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_bar_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_box_plot_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_heatmap_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_range_bar_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.12 → pam_python-0.2.13}/tests/test_typed_treemap_report.py +0 -0
|
@@ -21,6 +21,7 @@ COMMON_PROJECT_TEMPLATES = {
|
|
|
21
21
|
"buildcmd/pamb-base.sh": "pamb-base.sh",
|
|
22
22
|
"init/pylintrc.tmpl": ".pylintrc",
|
|
23
23
|
"init/REPORTS.md": "REPORTS.md",
|
|
24
|
+
"init/REPORTS-th.md": "REPORTS-th.md",
|
|
24
25
|
"init/AGENT.md": "AGENT.md",
|
|
25
26
|
"init/gitignore.tmpl": ".gitignore",
|
|
26
27
|
"init/dockerignore.tmpl": ".dockerignore",
|
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# Managed Reports สำหรับนักพัฒนา Data Plugin
|
|
2
|
+
|
|
3
|
+
คู่มือนี้อธิบายวิธีสร้าง แก้ไข และเผยแพร่ Managed Report ด้วย public Python API
|
|
4
|
+
ของ PAM นักพัฒนาไม่ต้องสร้างตาราง SQLite, renderer payload หรือ report JSON เอง
|
|
5
|
+
ชื่อ class, method, parameter และค่าคงที่ในตัวอย่างคงเป็นภาษาอังกฤษตาม API จริง
|
|
6
|
+
|
|
7
|
+
Framework รองรับ Report ต่อไปนี้
|
|
8
|
+
|
|
9
|
+
| กลุ่ม | Report | Public builder |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| PAM native | Table | `store.table(...)` |
|
|
12
|
+
| PAM native | Metric | `store.metric(...)` |
|
|
13
|
+
| Apex axis | Line | `store.line_chart(...)` |
|
|
14
|
+
| Apex axis | Area | `store.area_chart(...)` |
|
|
15
|
+
| Apex axis | Bar/Column | `store.bar_chart(...)` |
|
|
16
|
+
| Apex non-axis | Pie | `store.pie_chart(...)` |
|
|
17
|
+
| Apex non-axis | Donut | `store.donut_chart(...)` |
|
|
18
|
+
| Apex non-axis | Radial Bar | `store.radial_bar_chart(...)` |
|
|
19
|
+
| Apex axis | Scatter | `store.scatter_chart(...)` |
|
|
20
|
+
| Apex axis | Bubble | `store.bubble_chart(...)` |
|
|
21
|
+
| Apex matrix | Heatmap | `store.heatmap(...)` |
|
|
22
|
+
| Apex financial | Candlestick | `store.candlestick_chart(...)` |
|
|
23
|
+
| Apex statistical | Box Plot | `store.box_plot(...)` |
|
|
24
|
+
| Apex non-axis | Radar | `store.radar_chart(...)` |
|
|
25
|
+
| Apex non-axis | Polar Area | `store.polar_area_chart(...)` |
|
|
26
|
+
| Apex range | Range Bar | `store.range_bar_chart(...)` |
|
|
27
|
+
| Apex range | Range Area | `store.range_area_chart(...)` |
|
|
28
|
+
| Apex hierarchy | Treemap | `store.treemap(...)` |
|
|
29
|
+
|
|
30
|
+
ไม่มี raw report builder หรือช่องทางสร้าง custom report ด้วย JSON หากลูกค้าต้องการ
|
|
31
|
+
Report ชนิดใหม่ ต้องใช้ Framework รุ่นที่มี typed builder สำหรับ Report นั้น
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# 1. หลักการสำคัญ
|
|
36
|
+
|
|
37
|
+
## นักพัฒนาเลือกชนิด Report อย่างชัดเจน
|
|
38
|
+
|
|
39
|
+
Framework ไม่เดาชนิด Report จากข้อมูล นักพัฒนาต้องเลือกตาม requirement ที่ตกลง
|
|
40
|
+
กับลูกค้า โดยเรียก builder ที่ตรงกับความหมายของข้อมูล
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
store.table(...)
|
|
44
|
+
store.metric(...)
|
|
45
|
+
store.line_chart(...)
|
|
46
|
+
store.area_chart(...)
|
|
47
|
+
store.bar_chart(...)
|
|
48
|
+
store.pie_chart(...)
|
|
49
|
+
store.donut_chart(...)
|
|
50
|
+
store.radial_bar_chart(...)
|
|
51
|
+
store.scatter_chart(...)
|
|
52
|
+
store.bubble_chart(...)
|
|
53
|
+
store.heatmap(...)
|
|
54
|
+
store.candlestick_chart(...)
|
|
55
|
+
store.box_plot(...)
|
|
56
|
+
store.radar_chart(...)
|
|
57
|
+
store.polar_area_chart(...)
|
|
58
|
+
store.range_bar_chart(...)
|
|
59
|
+
store.range_area_chart(...)
|
|
60
|
+
store.treemap(...)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## นักพัฒนาไม่ระบุ renderer หรือ schema identifier
|
|
64
|
+
|
|
65
|
+
ห้ามกำหนดค่าทางเทคนิคเอง เช่น
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
renderer="table"
|
|
69
|
+
schema="customer/report-a@1"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Typed builder เป็นเจ้าของ renderer และ contract version รายละเอียดเหล่านี้เป็น
|
|
73
|
+
สัญญาภายในระหว่าง Framework กับ CMS
|
|
74
|
+
|
|
75
|
+
## ประกาศ Field เพียงครั้งเดียว
|
|
76
|
+
|
|
77
|
+
ชื่อแสดงผล ชนิดข้อมูล หน่วย และ nullability อยู่ใน Report definition ไม่ต้องส่งซ้ำ
|
|
78
|
+
ในทุก record
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
Measure(
|
|
82
|
+
"amount",
|
|
83
|
+
label="ยอดขาย",
|
|
84
|
+
value_type="number",
|
|
85
|
+
unit="THB",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
{"amount": 1250.50}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Typed writer จะแปลง declaration และข้อมูลเป็น managed definition, `dimensions`
|
|
92
|
+
และ `measures` นักพัฒนาห้ามสร้าง `definition_json`, `dimensions_json`,
|
|
93
|
+
`measures_json`, ตาราง SQLite หรือ renderer payload เอง
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# 2. พฤติกรรม Chart ที่รองรับ
|
|
98
|
+
|
|
99
|
+
- Column คือ Bar ที่ใช้ `orientation="vertical"`
|
|
100
|
+
- Horizontal Bar คือ Bar ที่ใช้ `orientation="horizontal"`
|
|
101
|
+
- Funnel ในรุ่นนี้เป็นตัวเลือกการแสดงผลของ Bar ไม่ใช่ top-level report type
|
|
102
|
+
- Mixed Chart ต้องมี public contract แยกก่อน Framework จึงจะรองรับ
|
|
103
|
+
- Straight, Smooth และ Stepline เป็นตัวเลือกการแสดงผลของ Line
|
|
104
|
+
- Pie และ Donut เป็นคนละ Apex type แต่ใช้โครงสร้าง slice แบบเดียวกัน
|
|
105
|
+
|
|
106
|
+
ห้ามประกอบ ApexCharts type อื่นผ่าน raw JSON
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
# 3. วงจรชีวิตของ Report Store
|
|
111
|
+
|
|
112
|
+
สร้าง Store ใหม่เฉพาะเมื่อผู้ใช้สั่ง fresh restart อย่างชัดเจน กรณีทั่วไปให้เปิด
|
|
113
|
+
Store เดิม หาก remote object ยังไม่มี Framework จะสร้าง Store แรกให้อัตโนมัติ
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
fresh = self.request.get_runtime_bool("fresh", default=False)
|
|
117
|
+
|
|
118
|
+
if fresh:
|
|
119
|
+
store = self.reports.create(
|
|
120
|
+
"reports.sqlite",
|
|
121
|
+
tracker_name="Customer Report Group",
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
store = self.reports.open(
|
|
125
|
+
"reports.sqlite",
|
|
126
|
+
tracker_name="Customer Report Group",
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`tracker_name` คือชื่อ Analytics Tracker group ใน PAM CMS ต้องใช้ชื่อเดิมสำหรับ
|
|
131
|
+
ทุก Report ใน Store หากไม่ระบุ Framework จะใช้ชื่อ Service
|
|
132
|
+
|
|
133
|
+
ประกาศ Report หลังเปิด Store เพียงครั้งเดียว สะสมข้อมูลจาก input page ได้หลายหน้า
|
|
134
|
+
และเรียก `publish()` ครั้งเดียวหลังคำนวณข้อมูลที่ต้องการครบและสำเร็จ
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
def on_start(self):
|
|
138
|
+
self.report_store = self.reports.open(
|
|
139
|
+
"reports.sqlite",
|
|
140
|
+
tracker_name="Customer Report Group",
|
|
141
|
+
)
|
|
142
|
+
self._declare_reports()
|
|
143
|
+
self._request_data_v2(file_format=RequestFileFormat.PARQUET)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def on_data_input(self, req):
|
|
147
|
+
self._append_page(req.data_input_v2)
|
|
148
|
+
if not req.data_input_v2.run_complete:
|
|
149
|
+
self._request_data_v2(
|
|
150
|
+
page=req.data_input_v2.next,
|
|
151
|
+
file_format=RequestFileFormat.PARQUET,
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
self.report_store.publish()
|
|
156
|
+
self._exit()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
การ publish จะ upload SQLite หนึ่งครั้ง และเผยแพร่ pointer ของ Report key ที่มี
|
|
160
|
+
การเปลี่ยนแปลงในรอบนั้น Remote read เป็น eventually consistent; Plugin ไม่ต้องรอ
|
|
161
|
+
หรือ poll จนผู้ใช้อ่าน Report ได้
|
|
162
|
+
|
|
163
|
+
Task lifecycle จะปิด manager และลบ local working files ให้อัตโนมัติ หากต้องการ
|
|
164
|
+
ทิ้ง Store ที่ยังเปิดก่อนเวลา ให้ใช้
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
self.reports.discard_local("reports.sqlite")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
คำสั่งนี้ไม่ลบ remote object และจะทิ้ง local change ที่ยังไม่ได้ publish
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
# 4. สัญญาร่วมของ Field
|
|
175
|
+
|
|
176
|
+
Import ที่ใช้บ่อย:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pam.reports import (
|
|
180
|
+
Dimension,
|
|
181
|
+
Measure,
|
|
182
|
+
Sum,
|
|
183
|
+
TemporalMode,
|
|
184
|
+
TimeRangeTransform,
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Dimension
|
|
189
|
+
|
|
190
|
+
Dimension ใช้ระบุ จัดกลุ่ม เรียง หรือแสดงชื่อของ data point
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
Dimension("branch", label="สาขา", value_type="string")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Measure
|
|
197
|
+
|
|
198
|
+
Measure คือค่าที่คำนวณแล้วและนำไปแสดงหรือ plot
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
Measure("amount", label="ยอดขาย", value_type="number", unit="THB")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Field ทุกตัวต้องมี:
|
|
205
|
+
|
|
206
|
+
| Property | ความหมาย |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `name` | key ที่คงที่และอ่านโดยระบบ แนะนำ `snake_case` |
|
|
209
|
+
| `label` | ชื่อที่ CMS แสดงต่อผู้ใช้ |
|
|
210
|
+
| `value_type` | ชนิดข้อมูลสำหรับ validation และ formatting |
|
|
211
|
+
|
|
212
|
+
Property เสริม:
|
|
213
|
+
|
|
214
|
+
| Property | Default | ความหมาย |
|
|
215
|
+
|---|---:|---|
|
|
216
|
+
| `required` | `True` | ทุก row/point ต้องมี field นี้หรือไม่ |
|
|
217
|
+
| `nullable` | `False` | ยอมให้ส่ง `None` อย่างชัดเจนหรือไม่ |
|
|
218
|
+
| `unit` | `None` | หน่วย เช่น `THB`, `people`, `orders`, `%` |
|
|
219
|
+
| `description` | `None` | คำอธิบายสำหรับนักพัฒนา |
|
|
220
|
+
|
|
221
|
+
ชนิดข้อมูลเริ่มต้น:
|
|
222
|
+
|
|
223
|
+
- `string`
|
|
224
|
+
- `integer`
|
|
225
|
+
- `number`
|
|
226
|
+
- `boolean`
|
|
227
|
+
- `date` — serialize เป็น `YYYY-MM-DD`
|
|
228
|
+
- `datetime` — เก็บเป็น epoch milliseconds; string input ภายใน PAM ต้องใช้
|
|
229
|
+
UTC format `YYYY-MM-DD HH:mm:ss`
|
|
230
|
+
|
|
231
|
+
ยังไม่รองรับ object และ array เป็นชนิด cell/point หากต้องใช้ข้อมูลซับซ้อนต้องเพิ่ม
|
|
232
|
+
Framework และ CMS contract อย่างชัดเจนก่อน
|
|
233
|
+
|
|
234
|
+
`label="สาขา"` อธิบาย field แต่ไม่ได้แปลค่า `BKK` เป็น `กรุงเทพฯ` หากต้องการทั้ง
|
|
235
|
+
code และชื่อแสดงผล ให้ประกาศคนละ field
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
Dimension("branch_code", label="รหัสสาขา", value_type="string")
|
|
239
|
+
Dimension("branch_name", label="สาขา", value_type="string")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
# 5. Range, Partition และความหมายของข้อมูลข้ามเวลา
|
|
245
|
+
|
|
246
|
+
หนึ่ง Report มี granularity คงที่เพียงค่าเดียว:
|
|
247
|
+
|
|
248
|
+
- `DAY`
|
|
249
|
+
- `MONTH`
|
|
250
|
+
- `NONE`
|
|
251
|
+
|
|
252
|
+
ใช้ `NONE` เมื่อ Report ไม่มี date range ให้ผู้ใช้เลือก `DAY` และ `MONTH` เก็บข้อมูล
|
|
253
|
+
แยกตาม partition โดย partition key ต้องมาจาก business date ในข้อมูล ไม่ใช่เวลา
|
|
254
|
+
ที่ Plugin เริ่มทำงาน
|
|
255
|
+
|
|
256
|
+
- `DAY` ใช้ `YYYY-MM-DD`
|
|
257
|
+
- `MONTH` ใช้ `YYYY-MM`
|
|
258
|
+
|
|
259
|
+
การคำนวณ partition เดิมซ้ำต้องแทนที่ข้อมูลทั้ง partition:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
table.delete_partition("2026-08")
|
|
263
|
+
table.append_rows(rows_from_page, partition_key="2026-08")
|
|
264
|
+
table.append_rows(more_rows, partition_key="2026-08")
|
|
265
|
+
|
|
266
|
+
# Publish หลังคำนวณครบและสำเร็จเท่านั้น
|
|
267
|
+
store.publish()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## เลือกความหมายให้ถูกก่อนกำหนด Temporal Mode
|
|
271
|
+
|
|
272
|
+
อย่าเลือกจากรูปทรง Chart เพราะ Pie, Bar หรือ Table อาจเก็บข้อมูลคนละความหมายได้
|
|
273
|
+
ให้พิจารณาว่าหนึ่ง partition หมายถึงอะไรในทางธุรกิจ
|
|
274
|
+
|
|
275
|
+
| ความหมายของหนึ่ง partition | Declaration | สิ่งที่ผู้ใช้ CMS เลือกได้ |
|
|
276
|
+
|---|---|---|
|
|
277
|
+
| ค่าของช่วงเวลานั้นซึ่งรวม เฉลี่ย หาค่าต่ำสุด/สูงสุดได้อย่างมีเหตุผล | `temporal_mode=TemporalMode.AGGREGATE` | Trend, Total, Average, Minimum, Maximum |
|
|
278
|
+
| สถานะทั้งชุด ณ เวลานั้น | `temporal_mode=TemporalMode.SNAPSHOT` | หนึ่งสถานะใน Chart เดิม หรือหลายสถานะเป็นแนวโน้ม |
|
|
279
|
+
| Business บังคับ bucket และ reducer ตายตัว | `range_transform=TimeRangeTransform(...)` | เลือกได้เฉพาะ bucket ที่ developer อนุญาต |
|
|
280
|
+
| ไม่ต้องตีความข้าม partition | ไม่ระบุทั้งคู่ | คงพฤติกรรมเดิมของ Report |
|
|
281
|
+
|
|
282
|
+
### `AGGREGATE`: ค่าประจำช่วงเวลา
|
|
283
|
+
|
|
284
|
+
ใช้เมื่อแต่ละ partition เป็นค่าที่เกิดขึ้นในช่วงนั้นและการคำนวณทุกแบบยังมีเหตุผล
|
|
285
|
+
เช่น ยอดขายรายวัน จำนวนคำสั่งซื้อรายเดือน หรือค่าใช้จ่ายแคมเปญรายวัน
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
sales = store.line_chart(
|
|
289
|
+
key="daily-sales",
|
|
290
|
+
granularity="DAY",
|
|
291
|
+
values=[Measure("sales_amount", label="ยอดขาย", value_type="number", unit="THB")],
|
|
292
|
+
temporal_mode=TemporalMode.AGGREGATE,
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Plugin ไม่เลือก `TREND`, `SUM`, `MEAN`, `MIN` หรือ `MAX` นักการตลาดเลือกใน CMS
|
|
297
|
+
หนึ่งตัวเลือกจะใช้กับทุก Measure ใน Report และค่าเริ่มต้นคือ `TREND`
|
|
298
|
+
|
|
299
|
+
อย่าใช้ `AGGREGATE` เพียงเพราะ Measure เป็นตัวเลข เช่น จำนวนสมาชิกปัจจุบันในแต่ละ
|
|
300
|
+
วันเป็นประชากรชุดเดิม การบวกหลายวันจะนับคนเดิมซ้ำ
|
|
301
|
+
|
|
302
|
+
### `SNAPSHOT`: สถานะทั้งชุด
|
|
303
|
+
|
|
304
|
+
ใช้เมื่อแต่ละ partition คือสถานะสมบูรณ์ ณ วันหรือเดือนนั้น เช่น สมาชิกปัจจุบันแยก
|
|
305
|
+
VIP/LOYAL/LOST, สินค้าคงคลังรายคลัง หรือการกระจาย tier ลูกค้า
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
members = store.pie_chart(
|
|
309
|
+
key="member-status",
|
|
310
|
+
granularity="DAY",
|
|
311
|
+
category=Dimension("status", label="สถานะ", value_type="string"),
|
|
312
|
+
value=Measure("members", label="สมาชิก", value_type="integer"),
|
|
313
|
+
temporal_mode=TemporalMode.SNAPSHOT,
|
|
314
|
+
)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
หนึ่ง partition แสดงด้วย Chart ที่ประกาศไว้ หลาย partition เรียงตามเวลาและ CMS
|
|
318
|
+
เลือก presentation ตาม temporal policy เช่น Pie หลายวันเปลี่ยนเป็น Stacked Area
|
|
319
|
+
category ที่หายจาก snapshot หนึ่งจะถูกแสดงเป็นศูนย์ หาก zero กับ missing มีความหมาย
|
|
320
|
+
ต่างกัน Plugin ต้องเขียน category ที่ตั้งใจให้เป็นศูนย์ให้ครบ
|
|
321
|
+
|
|
322
|
+
Snapshot หลายจุดแสดงว่าจำนวนรวมเปลี่ยน แต่พิสูจน์ transition รายบุคคลไม่ได้ เช่น
|
|
323
|
+
ไม่สามารถสรุปว่าใครย้ายจาก LOYAL ไป VIP ได้ Sankey ต้องใช้ source-to-target facts
|
|
324
|
+
ที่ประกาศอย่างชัดเจน
|
|
325
|
+
|
|
326
|
+
Temporal Mode รองรับ Table, Metric, Line, Area, Bar/Column, Pie, Donut,
|
|
327
|
+
Polar Area และ Radial Bar โดยมีกฎดังนี้
|
|
328
|
+
|
|
329
|
+
- Granularity ต้องเป็น `DAY` หรือ `MONTH`
|
|
330
|
+
- ต้องมี numeric Measure อย่างน้อยหนึ่งตัว
|
|
331
|
+
- ต้องใช้ `TemporalMode` enum ห้ามส่ง raw string เช่น `"SNAPSHOT"`
|
|
332
|
+
- `temporal_mode` และ `range_transform` ใช้พร้อมกันไม่ได้
|
|
333
|
+
- หนึ่ง Mode ใช้กับทั้ง Report ห้ามรวม Aggregate และ Snapshot Measure ใน Report เดียว
|
|
334
|
+
- partition เดิมต้องถูกลบและเขียนสถานะใหม่ทั้งชุดก่อน publish
|
|
335
|
+
|
|
336
|
+
### Fixed `range_transform`
|
|
337
|
+
|
|
338
|
+
ใช้เมื่อ requirement บังคับ reducer และ allowed bucket ตายตัว เช่น KPI ต้องรวมด้วย
|
|
339
|
+
SUM เสมอ หรือ compliance report ต้องเลือกค่ารายวันล่าสุดเสมอ หากต้องการให้นักการตลาด
|
|
340
|
+
สลับ Trend/Total/Average/Minimum/Maximum ให้ใช้ `TemporalMode.AGGREGATE`
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
line = store.line_chart(
|
|
344
|
+
key="sales-trend",
|
|
345
|
+
granularity="DAY",
|
|
346
|
+
values=[Measure("sales_amount", label="ยอดขาย", value_type="number")],
|
|
347
|
+
range_transform=TimeRangeTransform(
|
|
348
|
+
default_bucket="MONTH",
|
|
349
|
+
allowed_buckets=["DAY", "WEEK", "MONTH", "YEAR"],
|
|
350
|
+
reducers={"sales_amount": Sum()},
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Source granularity และ bucket ที่อนุญาต:
|
|
356
|
+
|
|
357
|
+
| Source | Allowed buckets |
|
|
358
|
+
|---|---|
|
|
359
|
+
| `DAY` | `DAY`, `WEEK`, `MONTH`, `YEAR`, `ALL` |
|
|
360
|
+
| `MONTH` | `MONTH`, `YEAR`, `ALL` |
|
|
361
|
+
| `NONE` | ไม่รองรับ range transform |
|
|
362
|
+
|
|
363
|
+
Output key:
|
|
364
|
+
|
|
365
|
+
- `DAY`: `YYYY-MM-DD`
|
|
366
|
+
- `WEEK`: ISO week `YYYY-Www` (จันทร์–อาทิตย์)
|
|
367
|
+
- `MONTH`: `YYYY-MM`
|
|
368
|
+
- `YEAR`: `YYYY`
|
|
369
|
+
- `ALL`: literal `ALL` ซึ่งหมายถึง range ที่ผู้ใช้เลือก ไม่ใช่ทุกวันใน Store
|
|
370
|
+
|
|
371
|
+
Reducer ที่รองรับ:
|
|
372
|
+
|
|
373
|
+
| Reducer | ความหมาย |
|
|
374
|
+
|---|---|
|
|
375
|
+
| `SUM` | บวก numeric value ที่มีอยู่ทั้งหมดใน bucket |
|
|
376
|
+
| `MEAN` | ค่าเฉลี่ยเลขคณิต โดยทุก source value มีน้ำหนักเท่ากัน |
|
|
377
|
+
| `MIN` | ค่าต่ำสุด |
|
|
378
|
+
| `MAX` | ค่าสูงสุด |
|
|
379
|
+
| `LAST` | ค่าจาก source partition ล่าสุดตาม canonical partition order |
|
|
380
|
+
|
|
381
|
+
`MEAN` ไม่ใช่ weighted average หากต้องการ `WEIGHTED_MEAN` หรือ `RATIO_OF_SUMS`
|
|
382
|
+
ต้องเพิ่ม contract ที่ระบุความสัมพันธ์ของ Measure ก่อน
|
|
383
|
+
|
|
384
|
+
เมื่อไม่ระบุทั้ง `range_transform` และ `temporal_mode` จะคงพฤติกรรมเดิม: Table
|
|
385
|
+
คืน record ที่เลือก, Metric คืน value group ต่อ partition, Line/Area เรียงตาม
|
|
386
|
+
partition และ `NONE` แสดง current snapshot โดยตรง
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
# 6. Table Report
|
|
391
|
+
|
|
392
|
+
เหมาะกับรายละเอียด Ranking, audit output หรือข้อมูลหลาย column
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
table = store.table(
|
|
396
|
+
key="member-summary",
|
|
397
|
+
granularity="MONTH",
|
|
398
|
+
columns=[
|
|
399
|
+
Dimension("branch", label="สาขา", value_type="string"),
|
|
400
|
+
Measure("members", label="สมาชิก", value_type="integer", unit="คน"),
|
|
401
|
+
],
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
table.append_rows(
|
|
405
|
+
[{"branch": "กรุงเทพฯ", "members": 1250}],
|
|
406
|
+
partition_key="2026-08",
|
|
407
|
+
)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
กฎ:
|
|
411
|
+
|
|
412
|
+
- ไม่รับ column ที่ไม่ได้ประกาศ
|
|
413
|
+
- Required column ต้องมีครบและค่าต้องตรงชนิด
|
|
414
|
+
- Row ใน Report เดียวกันห้ามมี generated identity ซ้ำ
|
|
415
|
+
- CMS ใช้ลำดับ column จาก definition ไม่ใช่ row แรก
|
|
416
|
+
- Partition เป็น metadata สำหรับ range/order ไม่ถูกเพิ่มเป็น Day/Month column พิเศษ
|
|
417
|
+
- `remove_rows` รับเฉพาะ Dimension identity; Measure ไม่ใช่ identity
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
# 7. Metric Report
|
|
422
|
+
|
|
423
|
+
เหมาะกับ KPI card หรือ headline number ที่ไม่มี Dimension
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
metric = store.metric(
|
|
427
|
+
key="monthly-kpis",
|
|
428
|
+
granularity="MONTH",
|
|
429
|
+
metrics=[
|
|
430
|
+
Measure("revenue", label="รายได้", value_type="number", unit="THB"),
|
|
431
|
+
Measure("orders", label="คำสั่งซื้อ", value_type="integer"),
|
|
432
|
+
],
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
metric.delete_partition("2026-08")
|
|
436
|
+
metric.set_values(
|
|
437
|
+
{"revenue": 125000.0, "orders": 320},
|
|
438
|
+
partition_key="2026-08",
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
ต้องมี Measure อย่างน้อยหนึ่งตัว ไม่รองรับ Dimension และหนึ่ง partition มี value set
|
|
443
|
+
ได้ชุดเดียว `set_values` ซ้ำใน DAY/MONTH ต้องลบ partition เดิมก่อน ส่วน `NONE`
|
|
444
|
+
จะ replace current value atomically
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
# 8. Line Chart Report
|
|
449
|
+
|
|
450
|
+
ใช้ partition เป็นแกนเวลา นักพัฒนาไม่ต้องประกาศ x ซ้ำ
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
line = store.line_chart(
|
|
454
|
+
key="daily-sales",
|
|
455
|
+
granularity="DAY",
|
|
456
|
+
values=[Measure("amount", label="ยอดขาย", value_type="number")],
|
|
457
|
+
series=Dimension("branch", label="สาขา", value_type="string"),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
line.append_points(
|
|
461
|
+
[{"branch": "กรุงเทพฯ", "amount": 1250}],
|
|
462
|
+
partition_key="2026-08-24",
|
|
463
|
+
)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
ต้องมี numeric Measure อย่างน้อยหนึ่งตัว `series` ต้องเป็น Dimension และ identity
|
|
467
|
+
คือ partition + optional series CMS เรียงตาม canonical partition ไม่ใช่ insertion order
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
# 9. Area Chart Report
|
|
472
|
+
|
|
473
|
+
ใช้ contract ของ Line และเพิ่ม `stacked` ซึ่งเป็น presentation metadata ไม่ใช่กฎ
|
|
474
|
+
การคำนวณ business value
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
area = store.area_chart(
|
|
478
|
+
key="member-trend",
|
|
479
|
+
granularity="DAY",
|
|
480
|
+
values=[Measure("members", label="สมาชิก", value_type="integer")],
|
|
481
|
+
series=Dimension("status", label="สถานะ", value_type="string"),
|
|
482
|
+
stacked=True,
|
|
483
|
+
)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
# 10. Bar และ Column Chart Report
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
bars = store.bar_chart(
|
|
492
|
+
key="sales-by-channel",
|
|
493
|
+
granularity="MONTH",
|
|
494
|
+
category=Dimension("channel", label="ช่องทาง", value_type="string"),
|
|
495
|
+
values=[Measure("amount", label="ยอดขาย", value_type="number")],
|
|
496
|
+
series=Dimension("branch", label="สาขา", value_type="string"),
|
|
497
|
+
orientation="vertical",
|
|
498
|
+
stacked=False,
|
|
499
|
+
)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
ต้องมี category Dimension หนึ่งตัวและ numeric Measure อย่างน้อยหนึ่งตัว `series`
|
|
503
|
+
ต้องเป็น Dimension, `orientation` เป็น `vertical` หรือ `horizontal` และห้ามมี
|
|
504
|
+
category/series identity ซ้ำ Partition เป็น metadata ไม่ถูกเติมหน้าชื่อ category
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
# 11. Pie และ Donut Chart Report
|
|
509
|
+
|
|
510
|
+
ใช้แสดงส่วนประกอบของ whole หนึ่งชุด ไม่เหมาะกับค่าติดลบ แนวโน้ม หรือ category มาก
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
pie = store.pie_chart(
|
|
514
|
+
key="members-by-status",
|
|
515
|
+
granularity="DAY",
|
|
516
|
+
category=Dimension("status", label="สถานะ", value_type="string"),
|
|
517
|
+
value=Measure("members", label="สมาชิก", value_type="integer"),
|
|
518
|
+
temporal_mode=TemporalMode.SNAPSHOT,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
pie.append_slices(
|
|
522
|
+
[{"status": "VIP", "members": 100}],
|
|
523
|
+
partition_key="2026-08-24",
|
|
524
|
+
)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
ต้องมี category หนึ่งตัวและ numeric Measure หนึ่งตัว ค่า finite และไม่ติดลบ ห้ามมี
|
|
528
|
+
category ซ้ำ Framework ไม่รวม category เล็กเป็น `Other` เอง Donut ใช้กฎข้อมูล
|
|
529
|
+
เดียวกับ Pie แต่ยังเป็นคนละ public type
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
# 12. Radial Bar Chart Report
|
|
534
|
+
|
|
535
|
+
ใช้กับ progress, completion หรือ score ที่มีขอบเขต
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
radial = store.radial_bar_chart(
|
|
539
|
+
key="completion",
|
|
540
|
+
granularity="MONTH",
|
|
541
|
+
metrics=[Measure("score", label="ความสำเร็จ", value_type="number")],
|
|
542
|
+
min_value=0,
|
|
543
|
+
max_value=100,
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
ต้องมี numeric Measure อย่างน้อยหนึ่งตัว ทุกค่าต้องอยู่ระหว่าง `min_value` และ
|
|
548
|
+
`max_value` และหนึ่ง partition มี value set เดียว Framework เก็บ business value จริง;
|
|
549
|
+
CMS เป็นผู้ normalize เป็นช่วง 0–100 ของ Apex
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
# 13. Scatter Chart Report
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
scatter = store.scatter_chart(
|
|
557
|
+
key="price-volume",
|
|
558
|
+
granularity="DAY",
|
|
559
|
+
x=Dimension("price", label="ราคา", value_type="number"),
|
|
560
|
+
y=Measure("volume", label="ปริมาณ", value_type="number"),
|
|
561
|
+
series=Dimension("product", label="สินค้า", value_type="string"),
|
|
562
|
+
)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
x และ y ต้องเป็น finite number, series เป็น Dimension Partition ใช้ filter source
|
|
566
|
+
facts แต่ไม่ใช่แกน Chart CMS รวม point จาก partition ที่เลือกไว้ใน Scatter เดียว
|
|
567
|
+
และไม่รองรับ `range_transform`
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
# 14. Bubble Chart Report
|
|
572
|
+
|
|
573
|
+
ใช้กฎ x, y และ series แบบ Scatter และเพิ่ม numeric `size` ที่ finite และไม่ติดลบ
|
|
574
|
+
`size` ไม่ใช่ identity และ selected partitions จะรวม raw bubbles ใน Chart เดียว
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
bubble = store.bubble_chart(
|
|
578
|
+
key="market-bubbles",
|
|
579
|
+
granularity="DAY",
|
|
580
|
+
x=Dimension("x", label="X", value_type="number"),
|
|
581
|
+
y=Measure("y", label="Y", value_type="number"),
|
|
582
|
+
size=Measure("size", label="ขนาด", value_type="number"),
|
|
583
|
+
)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
# 15. Heatmap Report
|
|
589
|
+
|
|
590
|
+
ต้องมี Dimension สองตัวและ numeric Measure หนึ่งตัว x/y coordinate ต้องไม่ซ้ำ
|
|
591
|
+
cell ที่ไม่มีข้อมูลจะยังคง missing ไม่เติมศูนย์ และแต่ละ partition แสดงเป็น grid
|
|
592
|
+
แยกกัน จึงไม่รองรับ `range_transform`
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
heatmap = store.heatmap(
|
|
596
|
+
key="activity",
|
|
597
|
+
granularity="DAY",
|
|
598
|
+
x=Dimension("hour", label="ชั่วโมง", value_type="integer"),
|
|
599
|
+
y=Dimension("weekday", label="วัน", value_type="string"),
|
|
600
|
+
value=Measure("events", label="เหตุการณ์", value_type="integer"),
|
|
601
|
+
)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
# 16. Candlestick Chart Report
|
|
607
|
+
|
|
608
|
+
x ต้องเป็น ordered date/datetime Dimension และมี finite numeric Measure สี่ตัว:
|
|
609
|
+
open, high, low, close โดยต้องเป็นไปตาม `low <= open <= high` และ
|
|
610
|
+
`low <= close <= high` ห้าม x ซ้ำ Selected partitions รวมเป็น Candlestick series
|
|
611
|
+
ที่เรียงแล้ว และ Framework ไม่เดา price reducer
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
# 17. Box Plot Report
|
|
616
|
+
|
|
617
|
+
ต้องมี category Dimension หนึ่งตัวและ numeric Measure ห้าตัว: minimum, q1, median,
|
|
618
|
+
q3, maximum โดยต้องเป็น `minimum <= q1 <= median <= q3 <= maximum` ห้าม category
|
|
619
|
+
ซ้ำ และแต่ละ partition แสดง Box Plot แยกกัน ไม่ merge distribution ข้าม partition
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
# 18. Radar Chart Report
|
|
624
|
+
|
|
625
|
+
ต้องมี axis Dimension หนึ่งตัวและ numeric Measure หนึ่งตัว optional series จะแยก
|
|
626
|
+
polygon แต่ละ axis/series ต้องไม่ซ้ำ `axis_order` ใช้กำหนดลำดับและปฏิเสธ axis
|
|
627
|
+
นอก declaration แต่ละ partition แสดง Radar หนึ่งชุดและไม่รองรับ `range_transform`
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
# 19. Polar Area Chart Report
|
|
632
|
+
|
|
633
|
+
ต้องมี category Dimension หนึ่งตัวและ numeric Measure ที่ไม่ติดลบหนึ่งตัว ห้าม
|
|
634
|
+
category ซ้ำ `NONE` คือ current snapshot หากใช้ fixed range transform สามารถใช้
|
|
635
|
+
`ALL` ได้เมื่อ developer ประกาศ reducer สำหรับ Measure นั้น
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
# 20. Range Bar Chart Report
|
|
640
|
+
|
|
641
|
+
ต้องมี category, start และ end โดย start/end ต้องเป็นชนิด numeric หรือ datetime
|
|
642
|
+
ชนิดเดียวกัน และ `end >= start` identity ของ category/series/range ห้ามซ้ำ
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
# 21. Range Area Chart Report
|
|
647
|
+
|
|
648
|
+
ต้องมี ordered x Dimension และ numeric Measure สองตัวคือ lower/upper โดย
|
|
649
|
+
`upper >= lower` แต่ละ x/series ต้องไม่ซ้ำ และ CMS เรียงตาม normalized x
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
# 22. Treemap Report
|
|
654
|
+
|
|
655
|
+
ต้องมี label Dimension และ factual numeric `value` หนึ่งตัว optional `size` ต้อง
|
|
656
|
+
เป็น numeric Measure ที่ไม่ติดลบ หากไม่ระบุ `size` ค่า `value` จะควบคุมทั้ง geometry
|
|
657
|
+
และค่าที่แสดง optional `group` ต้องเป็น Dimension และ label/group identity ห้ามซ้ำ
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
tree = store.treemap(
|
|
661
|
+
key="sales-by-brand",
|
|
662
|
+
granularity="DAY",
|
|
663
|
+
label=Dimension("brand", label="แบรนด์", value_type="string"),
|
|
664
|
+
value=Measure("sales", label="ยอดขาย", value_type="number"),
|
|
665
|
+
)
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
หาก `size` เป็น synthetic layout weight CMS ต้องไม่แสดงเสมือนเป็น factual value
|
|
669
|
+
`NONE` Treemap เป็น snapshot ส่วน partitioned Treemap ใช้ fixed `range_transform`
|
|
670
|
+
ตาม declaration ปัจจุบัน Treemap ยังไม่รองรับ temporal mode
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
# 23. กฎร่วมสำหรับการแก้ไขข้อมูล
|
|
675
|
+
|
|
676
|
+
| Report | เขียนข้อมูล | ลบเฉพาะรายการ |
|
|
677
|
+
|---|---|---|
|
|
678
|
+
| Table | `append_rows` | `remove_rows` |
|
|
679
|
+
| Metric | `set_values` | `delete_partition` หรือ `clear` |
|
|
680
|
+
| Line / Area | `append_points` | `remove_points` |
|
|
681
|
+
| Bar/Column | `append_bars` | `remove_bars` |
|
|
682
|
+
| Pie / Donut | `append_slices` | `remove_slices` |
|
|
683
|
+
| Radial Bar | `set_values` | `delete_partition` หรือ `clear` |
|
|
684
|
+
| Scatter | `append_points` | `remove_points` |
|
|
685
|
+
| Bubble | `append_bubbles` | `remove_bubbles` |
|
|
686
|
+
| Heatmap | `append_cells` | `remove_cells` |
|
|
687
|
+
| Candlestick | `append_candles` | `remove_candles` |
|
|
688
|
+
| Box Plot | `append_distributions` | `remove_distributions` |
|
|
689
|
+
| Radar | `append_points` | `remove_points` |
|
|
690
|
+
| Polar Area | `append_sectors` | `remove_sectors` |
|
|
691
|
+
| Range Bar / Range Area | `append_ranges` | `remove_ranges` |
|
|
692
|
+
| Treemap | `append_nodes` | `remove_nodes` |
|
|
693
|
+
|
|
694
|
+
ทุก Report รองรับ `delete_partition(partition_key)` สำหรับ DAY/MONTH, write method
|
|
695
|
+
ที่รับ `partition_key` อย่างชัดเจน และ `clear()` เพื่อลบ local records ก่อน publish
|
|
696
|
+
ถ้าคำนวณไม่สำเร็จห้าม publish; remote report จะไม่เปลี่ยน
|
|
697
|
+
|
|
698
|
+
Identity ที่ส่งให้ `remove_*`:
|
|
699
|
+
|
|
700
|
+
| Report | Identity fields |
|
|
701
|
+
|---|---|
|
|
702
|
+
| Table | Dimension ทุกตัวที่ประกาศ |
|
|
703
|
+
| Line / Area | optional `series`; หากไม่มีใช้ object ว่าง |
|
|
704
|
+
| Bar/Column | `category` และ optional `series` |
|
|
705
|
+
| Pie / Donut / Polar Area | `category` |
|
|
706
|
+
| Scatter | `x`, `y`, optional `series` |
|
|
707
|
+
| Bubble | `x`, `y`, optional `series`; ไม่รวม `size` |
|
|
708
|
+
| Heatmap | `x`, `y` |
|
|
709
|
+
| Candlestick | `x` |
|
|
710
|
+
| Box Plot | `category` |
|
|
711
|
+
| Radar | `axis`, optional `series` |
|
|
712
|
+
| Range Bar | `category`, optional `series`, `start`, `end` |
|
|
713
|
+
| Range Area | `x`, optional `series` |
|
|
714
|
+
| Treemap | `label`, optional `group` |
|
|
715
|
+
|
|
716
|
+
Metric และ Radial Bar มี value set เดียวต่อ partition จึง refresh ด้วย
|
|
717
|
+
`delete_partition` แล้วตามด้วย `set_values` สำหรับ `NONE`, `set_values` จะแทนที่
|
|
718
|
+
current snapshot
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
# 24. Validation และการจัดการความล้มเหลว
|
|
723
|
+
|
|
724
|
+
- Field ที่ไม่ได้ประกาศเป็น error และไม่ถูกละเลย
|
|
725
|
+
- Required field ที่หายเป็น error
|
|
726
|
+
- Boolean ไม่ถูกยอมรับเป็น integer หรือ number
|
|
727
|
+
- ปฏิเสธ `NaN`, positive infinity และ negative infinity
|
|
728
|
+
- Field name เป็น contract key ที่คงที่ เปลี่ยนได้เมื่อเปลี่ยน definition/version โดยตั้งใจ
|
|
729
|
+
- Report key เดิมห้ามเปลี่ยน report type, fields, granularity หรือ presentation options
|
|
730
|
+
- เมื่อคำนวณล้มเหลว ห้ามเรียก `store.publish()`
|
|
731
|
+
- ทุก write batch เป็น transaction; validation failure ต้องไม่ insert เพียงบางส่วน
|
|
732
|
+
- Error ต้องระบุ report key, field, expected type และตำแหน่งค่าที่ผิด โดยไม่ log
|
|
733
|
+
sensitive dataset ทั้งชุด
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
# 25. Report Type ที่ยังไม่รองรับ
|
|
738
|
+
|
|
739
|
+
Type นอก compatibility allowlist ถือว่าไม่รองรับ ห้ามเลียนแบบด้วยการเลือก renderer
|
|
740
|
+
หรือสร้าง raw JSON เอง Visual variant เช่น Mixed หรือ Funnel ต้องมี public contract
|
|
741
|
+
ก่อน แม้ ApexCharts ระดับล่างจะวาดได้ก็ตาม
|
|
742
|
+
|
|
743
|
+
หากลูกค้าต้องการ Report ชนิดใหม่ ให้หยุดและขอ Framework release ที่เพิ่ม typed
|
|
744
|
+
builder นั้น ห้ามเพิ่ม unregistered type ภายใน Plugin เพียงตัวเดียว
|
|
@@ -591,8 +591,9 @@ table.append_rows([
|
|
|
591
591
|
- Values must match their declared types.
|
|
592
592
|
- Two rows in the same report cannot have the same generated identity.
|
|
593
593
|
- CMS renders columns from the stored declaration, not from the first row.
|
|
594
|
-
- For `DAY` and `MONTH`,
|
|
595
|
-
Month column
|
|
594
|
+
- For `DAY` and `MONTH`, partition remains internal range and ordering metadata.
|
|
595
|
+
CMS does not add a special Day or Month column, and it never merges rows from
|
|
596
|
+
different partitions implicitly.
|
|
596
597
|
|
|
597
598
|
## Removing selected rows
|
|
598
599
|
|
|
@@ -55,6 +55,7 @@ pam/reports/treemap.py
|
|
|
55
55
|
pam/templates/buildcmd/pamb
|
|
56
56
|
pam/templates/buildcmd/pamb-base.sh
|
|
57
57
|
pam/templates/init/AGENT.md
|
|
58
|
+
pam/templates/init/REPORTS-th.md
|
|
58
59
|
pam/templates/init/REPORTS.md
|
|
59
60
|
pam/templates/init/dockerignore.tmpl
|
|
60
61
|
pam/templates/init/gitignore.tmpl
|
|
@@ -43,7 +43,13 @@ class CLITemplateTest(unittest.TestCase):
|
|
|
43
43
|
self.assertIn("pyarrow>=19.0.1", (root / "requirements.txt").read_text())
|
|
44
44
|
self.assertFalse((root / "pyproject.toml").exists())
|
|
45
45
|
self.assertTrue((root / "REPORTS.md").is_file())
|
|
46
|
+
self.assertTrue((root / "REPORTS-th.md").is_file())
|
|
46
47
|
reports_guide = (root / "REPORTS.md").read_text()
|
|
48
|
+
thai_reports_guide = (root / "REPORTS-th.md").read_text()
|
|
49
|
+
self.assertIn("Managed Reports สำหรับนักพัฒนา Data Plugin", thai_reports_guide)
|
|
50
|
+
self.assertIn("TemporalMode.AGGREGATE", thai_reports_guide)
|
|
51
|
+
self.assertIn("TemporalMode.SNAPSHOT", thai_reports_guide)
|
|
52
|
+
self.assertIn("อย่าเลือกจากรูปทรง Chart", thai_reports_guide)
|
|
47
53
|
self.assertIn("TemporalMode.AGGREGATE", reports_guide)
|
|
48
54
|
self.assertIn("TemporalMode.SNAPSHOT", reports_guide)
|
|
49
55
|
self.assertIn(
|
|
@@ -196,6 +202,8 @@ class CLITemplateTest(unittest.TestCase):
|
|
|
196
202
|
(root / "pyproject.toml").read_text(),
|
|
197
203
|
)
|
|
198
204
|
self.assertTrue((root / "uv.lock").is_file())
|
|
205
|
+
self.assertTrue((root / "REPORTS.md").is_file())
|
|
206
|
+
self.assertTrue((root / "REPORTS-th.md").is_file())
|
|
199
207
|
self.assertFalse((root / "requirements.txt").exists())
|
|
200
208
|
self.assertFalse((root / "run_unit_test.sh").exists())
|
|
201
209
|
self.assertIn(
|
|
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
|
|
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
|