pam-python 0.2.14__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.14/pam_python.egg-info → pam_python-0.2.15}/PKG-INFO +1 -1
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/__init__.py +1 -1
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/axis.py +28 -2
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/bar.py +38 -2
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/box_plot.py +19 -2
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/heatmap.py +34 -2
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/range_bar.py +37 -1
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/store.py +15 -2
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/REPORTS-th.md +10 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/REPORTS.md +18 -1
- {pam_python-0.2.14 → pam_python-0.2.15/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_bar_report.py +32 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_box_plot_report.py +21 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_heatmap_report.py +20 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_range_bar_report.py +21 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/LICENSE.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/README.md +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/api.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/cli.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/datetime_utils.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/interface_task_manager.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/logger.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/models/__init__.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/models/data_input_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/models/request_command.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/area.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/collection.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/definition.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/donut.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/errors.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/fields.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/line.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/manager.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/metric.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/pie.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/radar.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/record.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/schema.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/table.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/temporal.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/transport.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/request_file_format.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/server.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/service.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/sqlite.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/task_manager.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/AGENT.md +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/tester_task.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam/utils.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/pyproject.toml +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/setup.cfg +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_api.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_cli_templates.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_data_input_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_datetime_utils.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_report_store.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_temporal_mode.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.14 → pam_python-0.2.15}/tests/test_typed_treemap_report.py +0 -0
|
@@ -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(
|
|
@@ -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
|
|
|
@@ -813,6 +813,7 @@ bars = store.bar_chart(
|
|
|
813
813
|
],
|
|
814
814
|
orientation="vertical",
|
|
815
815
|
stacked=False,
|
|
816
|
+
category_order=["Online", "Store"],
|
|
816
817
|
range_transform=TimeRangeTransform(
|
|
817
818
|
default_bucket="MONTH",
|
|
818
819
|
allowed_buckets=["MONTH", "YEAR", "ALL"],
|
|
@@ -847,6 +848,8 @@ bars = store.bar_chart(
|
|
|
847
848
|
Measure("amount", label="Amount", value_type="number", unit="THB"),
|
|
848
849
|
],
|
|
849
850
|
stacked=True,
|
|
851
|
+
category_order=["Online", "Store"],
|
|
852
|
+
series_order=["Bangkok", "Chiang Mai"],
|
|
850
853
|
)
|
|
851
854
|
|
|
852
855
|
bars.delete_partition("2026-08")
|
|
@@ -864,6 +867,9 @@ bars.append_bars([
|
|
|
864
867
|
- `orientation` is `vertical` or `horizontal`.
|
|
865
868
|
- The optional series field must be a dimension.
|
|
866
869
|
- Duplicate category/series combinations are rejected.
|
|
870
|
+
- Optional `category_order` and `series_order` define the complete categorical
|
|
871
|
+
display order. Values written to an ordered dimension must appear in its declaration.
|
|
872
|
+
Without these options, CMS retains its default ascending label order.
|
|
867
873
|
- `NONE` creates a current snapshot and cannot declare `range_transform`.
|
|
868
874
|
- `DAY` and `MONTH` store facts under their business partition. When
|
|
869
875
|
`range_transform` is declared, CMS groups records by the selected time bucket plus
|
|
@@ -1077,6 +1083,8 @@ heatmap = store.heatmap(
|
|
|
1077
1083
|
x=Dimension("hour", label="Hour", value_type="integer"),
|
|
1078
1084
|
y=Dimension("weekday", label="Weekday", value_type="string"),
|
|
1079
1085
|
value=Measure("count", label="Count", value_type="integer"),
|
|
1086
|
+
x_order=[9, 10],
|
|
1087
|
+
y_order=["Monday", "Tuesday"],
|
|
1080
1088
|
)
|
|
1081
1089
|
|
|
1082
1090
|
heatmap.delete_partition("2026-08")
|
|
@@ -1095,6 +1103,8 @@ heatmap.append_cells([
|
|
|
1095
1103
|
- Each selected partition renders as an independent grid, so CMS never combines
|
|
1096
1104
|
equal x/y cells using an invented reducer. `NONE` stores one current snapshot.
|
|
1097
1105
|
- Heatmap does not declare `range_transform`.
|
|
1106
|
+
- Optional `x_order` and `y_order` define the complete categorical display order
|
|
1107
|
+
for each axis; undeclared coordinate values are rejected.
|
|
1098
1108
|
|
|
1099
1109
|
---
|
|
1100
1110
|
|
|
@@ -1148,12 +1158,13 @@ box_plot = store.box_plot(
|
|
|
1148
1158
|
median=Measure("median", label="Median", value_type="number"),
|
|
1149
1159
|
q3=Measure("q3", label="Q3", value_type="number"),
|
|
1150
1160
|
maximum=Measure("maximum", label="Maximum", value_type="number"),
|
|
1161
|
+
category_order=["VIP", "General"],
|
|
1151
1162
|
)
|
|
1152
1163
|
|
|
1153
1164
|
box_plot.delete_partition("2026-08")
|
|
1154
1165
|
|
|
1155
1166
|
box_plot.append_distributions([
|
|
1156
|
-
{"segment": "
|
|
1167
|
+
{"segment": "VIP", "minimum": 5, "q1": 10, "median": 15, "q3": 20, "maximum": 30},
|
|
1157
1168
|
], partition_key="2026-08")
|
|
1158
1169
|
```
|
|
1159
1170
|
|
|
@@ -1162,6 +1173,8 @@ box_plot.append_distributions([
|
|
|
1162
1173
|
- Exactly one category dimension and five numeric measures are required.
|
|
1163
1174
|
- Values must satisfy `minimum <= q1 <= median <= q3 <= maximum`.
|
|
1164
1175
|
- Duplicate categories are rejected.
|
|
1176
|
+
- Optional `category_order` defines the complete categorical display order;
|
|
1177
|
+
undeclared categories are rejected.
|
|
1165
1178
|
- Each partition renders as an independent Box Plot; distributions are not merged
|
|
1166
1179
|
across partitions by an invented statistical reducer.
|
|
1167
1180
|
|
|
@@ -1256,6 +1269,8 @@ ranges = store.range_bar_chart(
|
|
|
1256
1269
|
start=Measure("start", label="Start", value_type="datetime"),
|
|
1257
1270
|
end=Measure("end", label="End", value_type="datetime"),
|
|
1258
1271
|
series=Dimension("group", label="Group", value_type="string"),
|
|
1272
|
+
category_order=["Phase B", "Phase A"],
|
|
1273
|
+
series_order=["Project 2", "Project 1"],
|
|
1259
1274
|
)
|
|
1260
1275
|
|
|
1261
1276
|
ranges.delete_partition("2026-08")
|
|
@@ -1276,6 +1291,8 @@ ranges.append_ranges([
|
|
|
1276
1291
|
- Start and end use the same numeric or datetime value type.
|
|
1277
1292
|
- `end >= start`.
|
|
1278
1293
|
- Duplicate category/series/range identities are rejected.
|
|
1294
|
+
- Optional `category_order` and `series_order` define the complete categorical
|
|
1295
|
+
display order; undeclared values are rejected.
|
|
1279
1296
|
|
|
1280
1297
|
---
|
|
1281
1298
|
|
|
@@ -139,6 +139,38 @@ class TypedBarReportTest(unittest.TestCase):
|
|
|
139
139
|
self.bar(orientation="diagonal")
|
|
140
140
|
with self.assertRaisesRegex(ReportStoreValidationError, "stacked"):
|
|
141
141
|
self.bar(stacked="yes")
|
|
142
|
+
|
|
143
|
+
def test_explicit_category_and_series_order(self):
|
|
144
|
+
bar = self.store.bar_chart(
|
|
145
|
+
key="ordered-bar",
|
|
146
|
+
granularity="NONE",
|
|
147
|
+
category=Dimension("status", label="Status", value_type="string"),
|
|
148
|
+
series=Dimension("region", label="Region", value_type="string"),
|
|
149
|
+
values=[Measure("count", label="Count", value_type="integer")],
|
|
150
|
+
category_order=["Critical", "Warning", "Healthy"],
|
|
151
|
+
series_order=["North", "South"],
|
|
152
|
+
)
|
|
153
|
+
definition = json.loads(
|
|
154
|
+
self.store._connection.execute(
|
|
155
|
+
"SELECT definition_json FROM report_definitions WHERE report_key = ?",
|
|
156
|
+
("ordered-bar",),
|
|
157
|
+
).fetchone()[0]
|
|
158
|
+
)
|
|
159
|
+
self.assertEqual(
|
|
160
|
+
["Critical", "Warning", "Healthy"],
|
|
161
|
+
definition["options"]["category_order"],
|
|
162
|
+
)
|
|
163
|
+
self.assertEqual(["North", "South"], definition["options"]["series_order"])
|
|
164
|
+
bar.append_bars([{"status": "Critical", "region": "North", "count": 1}])
|
|
165
|
+
with self.assertRaisesRegex(ReportStoreValidationError, "category_order"):
|
|
166
|
+
bar.append_bars([{"status": "Unknown", "region": "North", "count": 1}])
|
|
167
|
+
with self.assertRaisesRegex(ReportStoreValidationError, "non-empty and unique"):
|
|
168
|
+
self.store.bar_chart(
|
|
169
|
+
"duplicate-order", "NONE",
|
|
170
|
+
Dimension("status", label="Status", value_type="string"),
|
|
171
|
+
[Measure("count", label="Count", value_type="integer")],
|
|
172
|
+
category_order=["A", "A"],
|
|
173
|
+
)
|
|
142
174
|
with self.assertRaisesRegex(ReportStoreValidationError, "integer or number"):
|
|
143
175
|
self.store.bar_chart(
|
|
144
176
|
"invalid-value",
|
|
@@ -67,6 +67,27 @@ class TypedBoxPlotReportTest(unittest.TestCase):
|
|
|
67
67
|
"box-plot-report", generate_box_plot_contract_fixture(fixture)["report_key"]
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
+
def test_explicit_category_order(self):
|
|
71
|
+
report = self.store.box_plot(
|
|
72
|
+
"ordered", "NONE",
|
|
73
|
+
Dimension("segment", label="Segment", value_type="string"),
|
|
74
|
+
Measure("minimum", label="Minimum", value_type="number"),
|
|
75
|
+
Measure("q1", label="Q1", value_type="number"),
|
|
76
|
+
Measure("median", label="Median", value_type="number"),
|
|
77
|
+
Measure("q3", label="Q3", value_type="number"),
|
|
78
|
+
Measure("maximum", label="Maximum", value_type="number"),
|
|
79
|
+
category_order=["VIP", "General"],
|
|
80
|
+
)
|
|
81
|
+
report.append_distributions([{
|
|
82
|
+
"segment": "VIP", "minimum": 1, "q1": 2, "median": 3,
|
|
83
|
+
"q3": 4, "maximum": 5,
|
|
84
|
+
}])
|
|
85
|
+
with self.assertRaisesRegex(ReportStoreValidationError, "category_order"):
|
|
86
|
+
report.append_distributions([{
|
|
87
|
+
"segment": "Unknown", "minimum": 1, "q1": 2, "median": 3,
|
|
88
|
+
"q3": 4, "maximum": 5,
|
|
89
|
+
}])
|
|
90
|
+
|
|
70
91
|
|
|
71
92
|
if __name__ == "__main__":
|
|
72
93
|
unittest.main()
|
|
@@ -52,5 +52,25 @@ class TypedHeatmapReportTest(unittest.TestCase):
|
|
|
52
52
|
fixture = Path(self.temp.name) / "heatmap.sqlite"
|
|
53
53
|
self.assertEqual("heatmap-report", generate_heatmap_contract_fixture(fixture)["report_key"])
|
|
54
54
|
|
|
55
|
+
def test_explicit_axis_orders(self):
|
|
56
|
+
heatmap = self.store.heatmap(
|
|
57
|
+
"ordered", "NONE",
|
|
58
|
+
Dimension("hour", label="Hour", value_type="integer"),
|
|
59
|
+
Dimension("weekday", label="Weekday", value_type="string"),
|
|
60
|
+
Measure("count", label="Count", value_type="integer"),
|
|
61
|
+
x_order=[9, 8],
|
|
62
|
+
y_order=["Friday", "Monday"],
|
|
63
|
+
)
|
|
64
|
+
heatmap.append_cells([{"hour": 9, "weekday": "Friday", "count": 1}])
|
|
65
|
+
definition = json.loads(
|
|
66
|
+
self.store._connection.execute(
|
|
67
|
+
"SELECT definition_json FROM report_definitions WHERE report_key = 'ordered'"
|
|
68
|
+
).fetchone()[0]
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual([9, 8], definition["options"]["x_order"])
|
|
71
|
+
self.assertEqual(["Friday", "Monday"], definition["options"]["y_order"])
|
|
72
|
+
with self.assertRaisesRegex(ReportStoreValidationError, "y_order"):
|
|
73
|
+
heatmap.append_cells([{"hour": 9, "weekday": "Sunday", "count": 1}])
|
|
74
|
+
|
|
55
75
|
|
|
56
76
|
if __name__ == "__main__": unittest.main()
|
|
@@ -102,6 +102,27 @@ class TypedRangeBarReportTest(unittest.TestCase):
|
|
|
102
102
|
Measure("end", label="End", value_type="number"),
|
|
103
103
|
)
|
|
104
104
|
|
|
105
|
+
def test_explicit_category_and_series_order(self):
|
|
106
|
+
report = self.store.range_bar_chart(
|
|
107
|
+
"ordered", "NONE",
|
|
108
|
+
Dimension("task", label="Task", value_type="string"),
|
|
109
|
+
Measure("start", label="Start", value_type="integer"),
|
|
110
|
+
Measure("end", label="End", value_type="integer"),
|
|
111
|
+
series=Dimension("group", label="Group", value_type="string"),
|
|
112
|
+
category_order=["Deploy", "Build"],
|
|
113
|
+
series_order=["P2", "P1"],
|
|
114
|
+
)
|
|
115
|
+
report.append_ranges([{"task": "Deploy", "group": "P2", "start": 1, "end": 2}])
|
|
116
|
+
definition = json.loads(
|
|
117
|
+
self.store._connection.execute(
|
|
118
|
+
"SELECT definition_json FROM report_definitions WHERE report_key = 'ordered'"
|
|
119
|
+
).fetchone()[0]
|
|
120
|
+
)
|
|
121
|
+
self.assertEqual(["Deploy", "Build"], definition["options"]["category_order"])
|
|
122
|
+
self.assertEqual(["P2", "P1"], definition["options"]["series_order"])
|
|
123
|
+
with self.assertRaisesRegex(ReportStoreValidationError, "series_order"):
|
|
124
|
+
report.append_ranges([{"task": "Deploy", "group": "P3", "start": 1, "end": 2}])
|
|
125
|
+
|
|
105
126
|
|
|
106
127
|
if __name__ == "__main__":
|
|
107
128
|
unittest.main()
|
|
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
|