pam-python 0.2.13__tar.gz → 0.2.15__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.13/pam_python.egg-info → pam_python-0.2.15}/PKG-INFO +4 -3
- {pam_python-0.2.13 → pam_python-0.2.15}/README.md +3 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/__init__.py +1 -1
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/models/data_input_v2.py +11 -1
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/axis.py +28 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/bar.py +38 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/box_plot.py +19 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/heatmap.py +34 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/range_bar.py +37 -1
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/store.py +15 -2
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/AGENT.md +4 -3
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/REPORTS-th.md +10 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/REPORTS.md +18 -1
- {pam_python-0.2.13 → pam_python-0.2.15/pam_python.egg-info}/PKG-INFO +4 -3
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_data_input_v2.py +52 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_bar_report.py +32 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_box_plot_report.py +21 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_heatmap_report.py +20 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_range_bar_report.py +21 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/LICENSE.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/api.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/cli.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/datetime_utils.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/interface_task_manager.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/logger.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/models/__init__.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/models/request_command.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/area.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/collection.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/definition.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/donut.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/errors.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/fields.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/line.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/manager.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/metric.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/pie.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/radar.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/record.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/schema.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/table.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/temporal.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/transport.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/request_file_format.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/server.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/service.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/sqlite.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/task_manager.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/tester_task.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam/utils.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/pyproject.toml +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/setup.cfg +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_api.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_cli_templates.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_datetime_utils.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_report_store.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_temporal_mode.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.13 → pam_python-0.2.15}/tests/test_typed_treemap_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pam-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Pam Python Library
|
|
5
5
|
Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
|
|
6
6
|
Project-URL: Homepage, https://github.com/heart/pam-python
|
|
@@ -134,8 +134,9 @@ See [Data Plugin Input V2](docs/data-plugin-input-v2.md).
|
|
|
134
134
|
|
|
135
135
|
New Pandas and Polars service projects are generated on the V2 lane by default;
|
|
136
136
|
legacy source can continue using V1 unchanged. For materialized Pandas frames,
|
|
137
|
-
use `data.read_population_pandas()` and
|
|
138
|
-
`data.dataset_at(position).read_pandas()
|
|
137
|
+
use `data.read_population_pandas()` and either
|
|
138
|
+
`data.dataset_at(position).read_pandas()` or
|
|
139
|
+
`data.dataset_named(name).read_pandas()`.
|
|
139
140
|
|
|
140
141
|
When your service is done:
|
|
141
142
|
|
|
@@ -114,8 +114,9 @@ See [Data Plugin Input V2](docs/data-plugin-input-v2.md).
|
|
|
114
114
|
|
|
115
115
|
New Pandas and Polars service projects are generated on the V2 lane by default;
|
|
116
116
|
legacy source can continue using V1 unchanged. For materialized Pandas frames,
|
|
117
|
-
use `data.read_population_pandas()` and
|
|
118
|
-
`data.dataset_at(position).read_pandas()
|
|
117
|
+
use `data.read_population_pandas()` and either
|
|
118
|
+
`data.dataset_at(position).read_pandas()` or
|
|
119
|
+
`data.dataset_named(name).read_pandas()`.
|
|
119
120
|
|
|
120
121
|
When your service is done:
|
|
121
122
|
|
|
@@ -142,6 +142,15 @@ class DataInputV2:
|
|
|
142
142
|
raise IndexError(f"V2 dataset position is out of range: {position}")
|
|
143
143
|
return self.datasets[position]
|
|
144
144
|
|
|
145
|
+
def dataset_named(self, name: str) -> DataInputV2Dataset:
|
|
146
|
+
"""Bind one configured dataset by an unambiguous manifest name."""
|
|
147
|
+
matches = [dataset for dataset in self.datasets if dataset.name == name]
|
|
148
|
+
if not matches:
|
|
149
|
+
raise KeyError(f"V2 dataset name is unknown: {name}")
|
|
150
|
+
if len(matches) > 1:
|
|
151
|
+
raise ValueError(f"V2 dataset name is ambiguous: {name}")
|
|
152
|
+
return matches[0]
|
|
153
|
+
|
|
145
154
|
|
|
146
155
|
def parse_data_input_v2(input_files: list[str]) -> DataInputV2:
|
|
147
156
|
files_by_name = _validate_flat_input_files(input_files)
|
|
@@ -202,6 +211,7 @@ def parse_data_input_v2(input_files: list[str]) -> DataInputV2:
|
|
|
202
211
|
if position in positions:
|
|
203
212
|
raise ValueError("V2 dataset position is duplicated")
|
|
204
213
|
positions.add(position)
|
|
214
|
+
dataset_name = _required_string(raw_dataset, "name")
|
|
205
215
|
dataset_format = _required_format(raw_dataset, "format")
|
|
206
216
|
raw_parts = raw_dataset.get("parts")
|
|
207
217
|
if not isinstance(raw_parts, list):
|
|
@@ -226,7 +236,7 @@ def parse_data_input_v2(input_files: list[str]) -> DataInputV2:
|
|
|
226
236
|
datasets.append(
|
|
227
237
|
DataInputV2Dataset(
|
|
228
238
|
position=position,
|
|
229
|
-
name=
|
|
239
|
+
name=dataset_name,
|
|
230
240
|
type=_required_string(raw_dataset, "type"),
|
|
231
241
|
format=dataset_format,
|
|
232
242
|
row_count=row_count,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Shared declarations for
|
|
1
|
+
"""Shared declarations for reports with ordered axes."""
|
|
2
2
|
|
|
3
|
-
from typing import Iterable, Optional, Tuple
|
|
3
|
+
from typing import Any, Iterable, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
from pam.reports.errors import ReportStoreValidationError
|
|
6
6
|
from pam.reports.fields import Dimension, Measure
|
|
@@ -9,6 +9,32 @@ from pam.reports.fields import Dimension, Measure
|
|
|
9
9
|
NUMERIC_TYPES = {"integer", "number"}
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def normalize_value_order(
|
|
13
|
+
report_name: str,
|
|
14
|
+
option_name: str,
|
|
15
|
+
dimension: Dimension,
|
|
16
|
+
values: Optional[Iterable[Any]],
|
|
17
|
+
) -> Optional[Tuple[Any, ...]]:
|
|
18
|
+
"""Normalize an optional complete order for a categorical dimension."""
|
|
19
|
+
if values is None:
|
|
20
|
+
return None
|
|
21
|
+
if isinstance(values, (str, bytes)):
|
|
22
|
+
raise ReportStoreValidationError(
|
|
23
|
+
f"{report_name} {option_name} must be an iterable of values"
|
|
24
|
+
)
|
|
25
|
+
try:
|
|
26
|
+
normalized = tuple(dimension.normalize_value(value) for value in values)
|
|
27
|
+
except TypeError as exc:
|
|
28
|
+
raise ReportStoreValidationError(
|
|
29
|
+
f"{report_name} {option_name} must be an iterable of values"
|
|
30
|
+
) from exc
|
|
31
|
+
if not normalized or len(normalized) != len(set(normalized)):
|
|
32
|
+
raise ReportStoreValidationError(
|
|
33
|
+
f"{report_name} {option_name} must be non-empty and unique"
|
|
34
|
+
)
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
|
|
12
38
|
def normalize_partition_series_declaration(
|
|
13
39
|
report_name: str,
|
|
14
40
|
values: Iterable[Measure],
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
4
4
|
|
|
5
|
-
from pam.reports.axis import NUMERIC_TYPES
|
|
5
|
+
from pam.reports.axis import NUMERIC_TYPES, normalize_value_order
|
|
6
6
|
from pam.reports.collection import TypedCollectionReport
|
|
7
7
|
from pam.reports.definition import ReportDefinition
|
|
8
8
|
from pam.reports.errors import ReportStoreValidationError
|
|
@@ -29,21 +29,36 @@ class BarChartReport(TypedCollectionReport):
|
|
|
29
29
|
series: Optional[Dimension],
|
|
30
30
|
orientation: str,
|
|
31
31
|
stacked: bool,
|
|
32
|
+
category_order: Optional[Sequence[Any]],
|
|
33
|
+
series_order: Optional[Sequence[Any]],
|
|
32
34
|
):
|
|
33
35
|
fields = (category,) + ((series,) if series is not None else ()) + tuple(values)
|
|
34
36
|
identity_fields = (category,) + ((series,) if series is not None else ())
|
|
37
|
+
def validate_order(item: Mapping[str, Any], _index: int) -> None:
|
|
38
|
+
if category_order is not None and item[category.name] not in category_order:
|
|
39
|
+
raise ReportStoreValidationError(
|
|
40
|
+
f"Bar Chart category value '{item[category.name]}' is not declared in category_order"
|
|
41
|
+
)
|
|
42
|
+
if series_order is not None and item[series.name] not in series_order:
|
|
43
|
+
raise ReportStoreValidationError(
|
|
44
|
+
f"Bar Chart series value '{item[series.name]}' is not declared in series_order"
|
|
45
|
+
)
|
|
46
|
+
|
|
35
47
|
super().__init__(
|
|
36
48
|
report=report,
|
|
37
49
|
granularity=granularity,
|
|
38
50
|
fields=fields,
|
|
39
51
|
identity_fields=identity_fields,
|
|
40
52
|
item_name="bar",
|
|
53
|
+
item_validator=validate_order,
|
|
41
54
|
)
|
|
42
55
|
self.category = category
|
|
43
56
|
self.values = tuple(values)
|
|
44
57
|
self.series = series
|
|
45
58
|
self.orientation = orientation
|
|
46
59
|
self.stacked = stacked
|
|
60
|
+
self.category_order = category_order
|
|
61
|
+
self.series_order = series_order
|
|
47
62
|
|
|
48
63
|
@classmethod
|
|
49
64
|
def declare(
|
|
@@ -58,6 +73,8 @@ class BarChartReport(TypedCollectionReport):
|
|
|
58
73
|
stacked: bool = False,
|
|
59
74
|
range_transform: Optional[TimeRangeTransform] = None,
|
|
60
75
|
temporal_mode: Optional[TemporalMode] = None,
|
|
76
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
77
|
+
series_order: Optional[Iterable[Any]] = None,
|
|
61
78
|
):
|
|
62
79
|
if not isinstance(category, Dimension):
|
|
63
80
|
raise ReportStoreValidationError(
|
|
@@ -107,6 +124,18 @@ class BarChartReport(TypedCollectionReport):
|
|
|
107
124
|
)
|
|
108
125
|
if not isinstance(stacked, bool):
|
|
109
126
|
raise ReportStoreValidationError("Bar Chart stacked must be boolean")
|
|
127
|
+
normalized_category_order = normalize_value_order(
|
|
128
|
+
"Bar Chart", "category_order", category, category_order
|
|
129
|
+
)
|
|
130
|
+
if series_order is not None and series is None:
|
|
131
|
+
raise ReportStoreValidationError(
|
|
132
|
+
"Bar Chart series_order requires a series Dimension"
|
|
133
|
+
)
|
|
134
|
+
normalized_series_order = (
|
|
135
|
+
normalize_value_order("Bar Chart", "series_order", series, series_order)
|
|
136
|
+
if series is not None
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
110
139
|
|
|
111
140
|
fields = (category,) + ((series,) if series is not None else ()) + normalized_values
|
|
112
141
|
normalized_range_transform = normalize_range_transform(
|
|
@@ -119,6 +148,11 @@ class BarChartReport(TypedCollectionReport):
|
|
|
119
148
|
"Bar Chart",
|
|
120
149
|
range_transform,
|
|
121
150
|
)
|
|
151
|
+
options = {"orientation": orientation, "stacked": stacked}
|
|
152
|
+
if normalized_category_order is not None:
|
|
153
|
+
options["category_order"] = list(normalized_category_order)
|
|
154
|
+
if normalized_series_order is not None:
|
|
155
|
+
options["series_order"] = list(normalized_series_order)
|
|
122
156
|
definition = ReportDefinition.create(
|
|
123
157
|
report_type="bar",
|
|
124
158
|
contract_version=BAR_CONTRACT_VERSION,
|
|
@@ -128,7 +162,7 @@ class BarChartReport(TypedCollectionReport):
|
|
|
128
162
|
"series": series.name if series is not None else None,
|
|
129
163
|
"values": [value.name for value in normalized_values],
|
|
130
164
|
},
|
|
131
|
-
options=
|
|
165
|
+
options=options,
|
|
132
166
|
range_transform=normalized_range_transform,
|
|
133
167
|
temporal_mode=normalized_temporal_mode,
|
|
134
168
|
)
|
|
@@ -147,6 +181,8 @@ class BarChartReport(TypedCollectionReport):
|
|
|
147
181
|
series,
|
|
148
182
|
orientation,
|
|
149
183
|
stacked,
|
|
184
|
+
normalized_category_order,
|
|
185
|
+
normalized_series_order,
|
|
150
186
|
)
|
|
151
187
|
|
|
152
188
|
def append_bars(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable, Mapping, Optional
|
|
4
4
|
|
|
5
|
-
from pam.reports.axis import NUMERIC_TYPES
|
|
5
|
+
from pam.reports.axis import NUMERIC_TYPES, normalize_value_order
|
|
6
6
|
from pam.reports.collection import TypedCollectionReport
|
|
7
7
|
from pam.reports.definition import ReportDefinition
|
|
8
8
|
from pam.reports.errors import ReportStoreValidationError
|
|
@@ -27,6 +27,7 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
27
27
|
median: Measure,
|
|
28
28
|
q3: Measure,
|
|
29
29
|
maximum: Measure,
|
|
30
|
+
category_order: Optional[Iterable[Any]],
|
|
30
31
|
):
|
|
31
32
|
values = (minimum, q1, median, q3, maximum)
|
|
32
33
|
fields = (category,) + values
|
|
@@ -37,6 +38,10 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
37
38
|
raise ReportStoreValidationError(
|
|
38
39
|
f"Box Plot distribution {index} must satisfy minimum <= q1 <= median <= q3 <= maximum"
|
|
39
40
|
)
|
|
41
|
+
if category_order is not None and item[category.name] not in category_order:
|
|
42
|
+
raise ReportStoreValidationError(
|
|
43
|
+
f"Box Plot category value '{item[category.name]}' is not declared in category_order"
|
|
44
|
+
)
|
|
40
45
|
|
|
41
46
|
super().__init__(
|
|
42
47
|
report=report,
|
|
@@ -52,6 +57,7 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
52
57
|
self.median = median
|
|
53
58
|
self.q3 = q3
|
|
54
59
|
self.maximum = maximum
|
|
60
|
+
self.category_order = category_order
|
|
55
61
|
|
|
56
62
|
@classmethod
|
|
57
63
|
def declare(
|
|
@@ -65,6 +71,7 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
65
71
|
median: Measure,
|
|
66
72
|
q3: Measure,
|
|
67
73
|
maximum: Measure,
|
|
74
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
68
75
|
):
|
|
69
76
|
if not isinstance(category, Dimension) or not category.required or category.nullable:
|
|
70
77
|
raise ReportStoreValidationError(
|
|
@@ -85,7 +92,13 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
85
92
|
names = [field.name for field in fields]
|
|
86
93
|
if len(names) != len(set(names)):
|
|
87
94
|
raise ReportStoreValidationError("Box Plot fields must use different names")
|
|
95
|
+
normalized_category_order = normalize_value_order(
|
|
96
|
+
"Box Plot", "category_order", category, category_order
|
|
97
|
+
)
|
|
88
98
|
|
|
99
|
+
options = {}
|
|
100
|
+
if normalized_category_order is not None:
|
|
101
|
+
options["category_order"] = list(normalized_category_order)
|
|
89
102
|
definition = ReportDefinition.create(
|
|
90
103
|
report_type="box_plot",
|
|
91
104
|
contract_version=BOX_PLOT_CONTRACT_VERSION,
|
|
@@ -98,11 +111,15 @@ class BoxPlotReport(TypedCollectionReport):
|
|
|
98
111
|
"q3": q3.name,
|
|
99
112
|
"maximum": maximum.name,
|
|
100
113
|
},
|
|
114
|
+
options=options,
|
|
101
115
|
)
|
|
102
116
|
report = store._declare_typed_report(
|
|
103
117
|
key, BOX_PLOT_RENDERER, BOX_PLOT_SCHEMA, granularity, definition.to_json()
|
|
104
118
|
)
|
|
105
|
-
return cls(
|
|
119
|
+
return cls(
|
|
120
|
+
report, granularity, category, minimum, q1, median, q3, maximum,
|
|
121
|
+
normalized_category_order,
|
|
122
|
+
)
|
|
106
123
|
|
|
107
124
|
def append_distributions(
|
|
108
125
|
self,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable, Mapping, Optional
|
|
4
4
|
|
|
5
|
-
from pam.reports.axis import NUMERIC_TYPES
|
|
5
|
+
from pam.reports.axis import NUMERIC_TYPES, normalize_value_order
|
|
6
6
|
from pam.reports.collection import TypedCollectionReport
|
|
7
7
|
from pam.reports.definition import ReportDefinition
|
|
8
8
|
from pam.reports.errors import ReportStoreValidationError
|
|
@@ -22,17 +22,32 @@ class HeatmapReport(TypedCollectionReport):
|
|
|
22
22
|
x: Dimension,
|
|
23
23
|
y: Dimension,
|
|
24
24
|
value: Measure,
|
|
25
|
+
x_order: Optional[Iterable[Any]],
|
|
26
|
+
y_order: Optional[Iterable[Any]],
|
|
25
27
|
):
|
|
28
|
+
def validate_order(item: Mapping[str, Any], _index: int) -> None:
|
|
29
|
+
if x_order is not None and item[x.name] not in x_order:
|
|
30
|
+
raise ReportStoreValidationError(
|
|
31
|
+
f"Heatmap x value '{item[x.name]}' is not declared in x_order"
|
|
32
|
+
)
|
|
33
|
+
if y_order is not None and item[y.name] not in y_order:
|
|
34
|
+
raise ReportStoreValidationError(
|
|
35
|
+
f"Heatmap y value '{item[y.name]}' is not declared in y_order"
|
|
36
|
+
)
|
|
37
|
+
|
|
26
38
|
super().__init__(
|
|
27
39
|
report=report,
|
|
28
40
|
granularity=granularity,
|
|
29
41
|
fields=(x, y, value),
|
|
30
42
|
identity_fields=(x, y),
|
|
31
43
|
item_name="cell",
|
|
44
|
+
item_validator=validate_order,
|
|
32
45
|
)
|
|
33
46
|
self.x = x
|
|
34
47
|
self.y = y
|
|
35
48
|
self.value = value
|
|
49
|
+
self.x_order = x_order
|
|
50
|
+
self.y_order = y_order
|
|
36
51
|
|
|
37
52
|
@classmethod
|
|
38
53
|
def declare(
|
|
@@ -43,6 +58,8 @@ class HeatmapReport(TypedCollectionReport):
|
|
|
43
58
|
x: Dimension,
|
|
44
59
|
y: Dimension,
|
|
45
60
|
value: Measure,
|
|
61
|
+
x_order: Optional[Iterable[Any]] = None,
|
|
62
|
+
y_order: Optional[Iterable[Any]] = None,
|
|
46
63
|
):
|
|
47
64
|
for name, field in (("x", x), ("y", y)):
|
|
48
65
|
if not isinstance(field, Dimension):
|
|
@@ -61,17 +78,32 @@ class HeatmapReport(TypedCollectionReport):
|
|
|
61
78
|
raise ReportStoreValidationError("Heatmap value must be required and non-nullable")
|
|
62
79
|
if len({x.name, y.name, value.name}) != 3:
|
|
63
80
|
raise ReportStoreValidationError("Heatmap fields must use different names")
|
|
81
|
+
normalized_x_order = normalize_value_order(
|
|
82
|
+
"Heatmap", "x_order", x, x_order
|
|
83
|
+
)
|
|
84
|
+
normalized_y_order = normalize_value_order(
|
|
85
|
+
"Heatmap", "y_order", y, y_order
|
|
86
|
+
)
|
|
64
87
|
|
|
88
|
+
options = {}
|
|
89
|
+
if normalized_x_order is not None:
|
|
90
|
+
options["x_order"] = list(normalized_x_order)
|
|
91
|
+
if normalized_y_order is not None:
|
|
92
|
+
options["y_order"] = list(normalized_y_order)
|
|
65
93
|
definition = ReportDefinition.create(
|
|
66
94
|
report_type="heatmap",
|
|
67
95
|
contract_version=HEATMAP_CONTRACT_VERSION,
|
|
68
96
|
fields=(x, y, value),
|
|
69
97
|
binding={"x": x.name, "y": y.name, "value": value.name},
|
|
98
|
+
options=options,
|
|
70
99
|
)
|
|
71
100
|
report = store._declare_typed_report(
|
|
72
101
|
key, HEATMAP_RENDERER, HEATMAP_SCHEMA, granularity, definition.to_json()
|
|
73
102
|
)
|
|
74
|
-
return cls(
|
|
103
|
+
return cls(
|
|
104
|
+
report, granularity, x, y, value,
|
|
105
|
+
normalized_x_order, normalized_y_order,
|
|
106
|
+
)
|
|
75
107
|
|
|
76
108
|
def append_cells(
|
|
77
109
|
self,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable, Mapping, Optional
|
|
4
4
|
|
|
5
|
+
from pam.reports.axis import normalize_value_order
|
|
5
6
|
from pam.reports.collection import TypedCollectionReport
|
|
6
7
|
from pam.reports.definition import ReportDefinition
|
|
7
8
|
from pam.reports.errors import ReportStoreValidationError
|
|
@@ -25,6 +26,8 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
25
26
|
start: Measure,
|
|
26
27
|
end: Measure,
|
|
27
28
|
series: Optional[Dimension],
|
|
29
|
+
category_order: Optional[Iterable[Any]],
|
|
30
|
+
series_order: Optional[Iterable[Any]],
|
|
28
31
|
):
|
|
29
32
|
fields = (category,) + ((series,) if series is not None else ()) + (start, end)
|
|
30
33
|
identity_fields = (
|
|
@@ -36,6 +39,14 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
36
39
|
raise ReportStoreValidationError(
|
|
37
40
|
f"Range Bar range {index} field '{end.name}' must be greater than or equal to '{start.name}'"
|
|
38
41
|
)
|
|
42
|
+
if category_order is not None and item[category.name] not in category_order:
|
|
43
|
+
raise ReportStoreValidationError(
|
|
44
|
+
f"Range Bar category value '{item[category.name]}' is not declared in category_order"
|
|
45
|
+
)
|
|
46
|
+
if series_order is not None and item[series.name] not in series_order:
|
|
47
|
+
raise ReportStoreValidationError(
|
|
48
|
+
f"Range Bar series value '{item[series.name]}' is not declared in series_order"
|
|
49
|
+
)
|
|
39
50
|
|
|
40
51
|
super().__init__(
|
|
41
52
|
report=report,
|
|
@@ -49,6 +60,8 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
49
60
|
self.start = start
|
|
50
61
|
self.end = end
|
|
51
62
|
self.series = series
|
|
63
|
+
self.category_order = category_order
|
|
64
|
+
self.series_order = series_order
|
|
52
65
|
|
|
53
66
|
@classmethod
|
|
54
67
|
def declare(
|
|
@@ -60,6 +73,8 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
60
73
|
start: Measure,
|
|
61
74
|
end: Measure,
|
|
62
75
|
series: Optional[Dimension] = None,
|
|
76
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
77
|
+
series_order: Optional[Iterable[Any]] = None,
|
|
63
78
|
):
|
|
64
79
|
if not isinstance(category, Dimension) or not category.required or category.nullable:
|
|
65
80
|
raise ReportStoreValidationError(
|
|
@@ -89,7 +104,24 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
89
104
|
names = [field.name for field in fields]
|
|
90
105
|
if len(names) != len(set(names)):
|
|
91
106
|
raise ReportStoreValidationError("Range Bar fields must use different names")
|
|
107
|
+
normalized_category_order = normalize_value_order(
|
|
108
|
+
"Range Bar", "category_order", category, category_order
|
|
109
|
+
)
|
|
110
|
+
if series_order is not None and series is None:
|
|
111
|
+
raise ReportStoreValidationError(
|
|
112
|
+
"Range Bar series_order requires a series Dimension"
|
|
113
|
+
)
|
|
114
|
+
normalized_series_order = (
|
|
115
|
+
normalize_value_order("Range Bar", "series_order", series, series_order)
|
|
116
|
+
if series is not None
|
|
117
|
+
else None
|
|
118
|
+
)
|
|
92
119
|
|
|
120
|
+
options = {}
|
|
121
|
+
if normalized_category_order is not None:
|
|
122
|
+
options["category_order"] = list(normalized_category_order)
|
|
123
|
+
if normalized_series_order is not None:
|
|
124
|
+
options["series_order"] = list(normalized_series_order)
|
|
93
125
|
definition = ReportDefinition.create(
|
|
94
126
|
report_type="range_bar",
|
|
95
127
|
contract_version=RANGE_BAR_CONTRACT_VERSION,
|
|
@@ -100,11 +132,15 @@ class RangeBarChartReport(TypedCollectionReport):
|
|
|
100
132
|
"end": end.name,
|
|
101
133
|
"series": series.name if series is not None else None,
|
|
102
134
|
},
|
|
135
|
+
options=options,
|
|
103
136
|
)
|
|
104
137
|
report = store._declare_typed_report(
|
|
105
138
|
key, RANGE_BAR_RENDERER, RANGE_BAR_SCHEMA, granularity, definition.to_json()
|
|
106
139
|
)
|
|
107
|
-
return cls(
|
|
140
|
+
return cls(
|
|
141
|
+
report, granularity, category, start, end, series,
|
|
142
|
+
normalized_category_order, normalized_series_order,
|
|
143
|
+
)
|
|
108
144
|
|
|
109
145
|
def append_ranges(
|
|
110
146
|
self,
|
|
@@ -221,6 +221,8 @@ class ReportStore:
|
|
|
221
221
|
stacked: bool = False,
|
|
222
222
|
range_transform: Optional[TimeRangeTransform] = None,
|
|
223
223
|
temporal_mode: Optional[TemporalMode] = None,
|
|
224
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
225
|
+
series_order: Optional[Iterable[Any]] = None,
|
|
224
226
|
) -> BarChartReport:
|
|
225
227
|
"""Declare a typed Bar/Column Chart report."""
|
|
226
228
|
return BarChartReport.declare(
|
|
@@ -234,6 +236,8 @@ class ReportStore:
|
|
|
234
236
|
stacked,
|
|
235
237
|
range_transform,
|
|
236
238
|
temporal_mode,
|
|
239
|
+
category_order,
|
|
240
|
+
series_order,
|
|
237
241
|
)
|
|
238
242
|
|
|
239
243
|
def pie_chart(
|
|
@@ -355,6 +359,7 @@ class ReportStore:
|
|
|
355
359
|
median: Measure,
|
|
356
360
|
q3: Measure,
|
|
357
361
|
maximum: Measure,
|
|
362
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
358
363
|
) -> BoxPlotReport:
|
|
359
364
|
"""Declare a typed Box Plot report."""
|
|
360
365
|
return BoxPlotReport.declare(
|
|
@@ -367,6 +372,7 @@ class ReportStore:
|
|
|
367
372
|
median,
|
|
368
373
|
q3,
|
|
369
374
|
maximum,
|
|
375
|
+
category_order,
|
|
370
376
|
)
|
|
371
377
|
|
|
372
378
|
def heatmap(
|
|
@@ -376,9 +382,13 @@ class ReportStore:
|
|
|
376
382
|
x: Dimension,
|
|
377
383
|
y: Dimension,
|
|
378
384
|
value: Measure,
|
|
385
|
+
x_order: Optional[Iterable[Any]] = None,
|
|
386
|
+
y_order: Optional[Iterable[Any]] = None,
|
|
379
387
|
) -> HeatmapReport:
|
|
380
388
|
"""Declare a typed Heatmap report."""
|
|
381
|
-
return HeatmapReport.declare(
|
|
389
|
+
return HeatmapReport.declare(
|
|
390
|
+
self, key, granularity, x, y, value, x_order, y_order
|
|
391
|
+
)
|
|
382
392
|
|
|
383
393
|
def radar_chart(
|
|
384
394
|
self,
|
|
@@ -402,10 +412,13 @@ class ReportStore:
|
|
|
402
412
|
start: Measure,
|
|
403
413
|
end: Measure,
|
|
404
414
|
series: Optional[Dimension] = None,
|
|
415
|
+
category_order: Optional[Iterable[Any]] = None,
|
|
416
|
+
series_order: Optional[Iterable[Any]] = None,
|
|
405
417
|
) -> RangeBarChartReport:
|
|
406
418
|
"""Declare a typed Range Bar Chart report."""
|
|
407
419
|
return RangeBarChartReport.declare(
|
|
408
|
-
self, key, granularity, category, start, end, series
|
|
420
|
+
self, key, granularity, category, start, end, series,
|
|
421
|
+
category_order, series_order,
|
|
409
422
|
)
|
|
410
423
|
|
|
411
424
|
def range_area_chart(
|
|
@@ -215,15 +215,16 @@ V2 is a separate, explicit lane. Call `_request_data_v2(...)` with CSV or
|
|
|
215
215
|
Parquet when the PAM adapter is configured for V2. The requested format applies
|
|
216
216
|
to that page and may change on a later request. In `on_data_input`, require
|
|
217
217
|
`req.data_input_v2`, validate the expected dataset count, and bind each dataset
|
|
218
|
-
by its configured position
|
|
218
|
+
by an unambiguous manifest name (or by its configured position when names are
|
|
219
|
+
not unique or the contract requires positional binding):
|
|
219
220
|
|
|
220
221
|
```python
|
|
221
222
|
data = req.data_input_v2
|
|
222
223
|
if data is None or len(data.datasets) != 2:
|
|
223
224
|
raise ValueError("Expected V2 profile and purchase datasets")
|
|
224
225
|
|
|
225
|
-
profile = data.
|
|
226
|
-
purchase = data.
|
|
226
|
+
profile = data.dataset_named("profile")
|
|
227
|
+
purchase = data.dataset_named("purchase")
|
|
227
228
|
|
|
228
229
|
population = data.scan_population_polars()
|
|
229
230
|
profile_frame = profile.scan_polars()
|
|
@@ -496,12 +496,16 @@ bars = store.bar_chart(
|
|
|
496
496
|
series=Dimension("branch", label="สาขา", value_type="string"),
|
|
497
497
|
orientation="vertical",
|
|
498
498
|
stacked=False,
|
|
499
|
+
category_order=["Online", "Store"],
|
|
500
|
+
series_order=["Bangkok", "Chiang Mai"],
|
|
499
501
|
)
|
|
500
502
|
```
|
|
501
503
|
|
|
502
504
|
ต้องมี category Dimension หนึ่งตัวและ numeric Measure อย่างน้อยหนึ่งตัว `series`
|
|
503
505
|
ต้องเป็น Dimension, `orientation` เป็น `vertical` หรือ `horizontal` และห้ามมี
|
|
504
506
|
category/series identity ซ้ำ Partition เป็น metadata ไม่ถูกเติมหน้าชื่อ category
|
|
507
|
+
กำหนดลำดับเองได้ด้วย `category_order` และ `series_order` โดยรายการที่ระบุถือเป็น
|
|
508
|
+
รายการค่าที่อนุญาตทั้งหมด หากไม่กำหนด CMS จะเรียง label แบบ ascending ตามเดิม
|
|
505
509
|
|
|
506
510
|
---
|
|
507
511
|
|
|
@@ -598,9 +602,13 @@ heatmap = store.heatmap(
|
|
|
598
602
|
x=Dimension("hour", label="ชั่วโมง", value_type="integer"),
|
|
599
603
|
y=Dimension("weekday", label="วัน", value_type="string"),
|
|
600
604
|
value=Measure("events", label="เหตุการณ์", value_type="integer"),
|
|
605
|
+
x_order=[9, 10],
|
|
606
|
+
y_order=["Monday", "Tuesday"],
|
|
601
607
|
)
|
|
602
608
|
```
|
|
603
609
|
|
|
610
|
+
`x_order` และ `y_order` ใช้กำหนดลำดับแกน โดยค่าที่เขียนต้องอยู่ในรายการที่ประกาศ
|
|
611
|
+
|
|
604
612
|
---
|
|
605
613
|
|
|
606
614
|
# 16. Candlestick Chart Report
|
|
@@ -617,6 +625,7 @@ open, high, low, close โดยต้องเป็นไปตาม `low <=
|
|
|
617
625
|
ต้องมี category Dimension หนึ่งตัวและ numeric Measure ห้าตัว: minimum, q1, median,
|
|
618
626
|
q3, maximum โดยต้องเป็น `minimum <= q1 <= median <= q3 <= maximum` ห้าม category
|
|
619
627
|
ซ้ำ และแต่ละ partition แสดง Box Plot แยกกัน ไม่ merge distribution ข้าม partition
|
|
628
|
+
กำหนดลำดับ category ได้ด้วย `category_order=[...]` และค่าที่เขียนต้องอยู่ในรายการ
|
|
620
629
|
|
|
621
630
|
---
|
|
622
631
|
|
|
@@ -640,6 +649,7 @@ category ซ้ำ `NONE` คือ current snapshot หากใช้ fixed ra
|
|
|
640
649
|
|
|
641
650
|
ต้องมี category, start และ end โดย start/end ต้องเป็นชนิด numeric หรือ datetime
|
|
642
651
|
ชนิดเดียวกัน และ `end >= start` identity ของ category/series/range ห้ามซ้ำ
|
|
652
|
+
กำหนดลำดับได้ด้วย `category_order` และ `series_order`; ค่าที่เขียนต้องอยู่ในรายการ
|
|
643
653
|
|
|
644
654
|
---
|
|
645
655
|
|