173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""报表图表数据聚合(对齐 JNPF ChartUtil)"""
|
||
from __future__ import annotations
|
||
|
||
from collections import defaultdict
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||
|
||
|
||
def _parse_field(field: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
|
||
"""alias.field -> (alias, field_name)"""
|
||
if not field or not isinstance(field, str):
|
||
return None, None
|
||
parts = field.split(".", 1)
|
||
if len(parts) == 2:
|
||
return parts[0], parts[1]
|
||
return None, field
|
||
|
||
|
||
def _collect_rows(datasets: Dict[str, List[Dict[str, Any]]], dataset_names: Set[str]) -> List[Dict[str, Any]]:
|
||
rows: List[Dict[str, Any]] = []
|
||
for name in dataset_names:
|
||
rows.extend(datasets.get(name) or [])
|
||
return rows
|
||
|
||
|
||
def _aggregate(values: List[Any], summary_type: str) -> str:
|
||
if not values:
|
||
return ""
|
||
st = (summary_type or "none").lower()
|
||
nums: List[Decimal] = []
|
||
for v in values:
|
||
try:
|
||
nums.append(Decimal(str(v)))
|
||
except Exception:
|
||
pass
|
||
if st == "sum" and nums:
|
||
return str(sum(nums))
|
||
if st == "avg" and nums:
|
||
return str(sum(nums) / len(nums))
|
||
if st == "max" and nums:
|
||
return str(max(nums))
|
||
if st == "min" and nums:
|
||
return str(min(nums))
|
||
if st == "count":
|
||
return str(len(values))
|
||
return str(values[-1]) if values else ""
|
||
|
||
|
||
def _build_field(
|
||
data_list: List[Dict[str, Any]],
|
||
classify_key: Optional[str],
|
||
series_name_key: Optional[str],
|
||
series_data_key: Optional[str],
|
||
max_key: Optional[str],
|
||
summary_type: str,
|
||
) -> Dict[str, Any]:
|
||
chart_map: Dict[Any, Dict[Any, List[Any]]] = defaultdict(lambda: defaultdict(list))
|
||
max_map: Dict[Any, List[Any]] = defaultdict(list)
|
||
|
||
for row in data_list:
|
||
if not classify_key:
|
||
continue
|
||
classify = row.get(classify_key)
|
||
if classify is None:
|
||
continue
|
||
value = row.get(series_data_key) if series_data_key else None
|
||
if value is None:
|
||
continue
|
||
series = row.get(series_name_key) if series_name_key else ""
|
||
chart_map[series][classify].append(value)
|
||
if max_key:
|
||
mx = row.get(max_key)
|
||
if mx is not None:
|
||
max_map[classify].append(mx)
|
||
|
||
series_name_list: List[str] = []
|
||
classify_map: Dict[Any, List[List[str]]] = defaultdict(list)
|
||
max_counts = [0]
|
||
|
||
for series, classify_name_map in chart_map.items():
|
||
series_name_list.append(str(series))
|
||
for classify, value_list in classify_name_map.items():
|
||
agg = _aggregate(value_list, summary_type)
|
||
classify_map[classify].append([agg])
|
||
max_counts.append(len(classify_map[classify]))
|
||
|
||
classify_name_list = sorted(str(k) for k in classify_map.keys())
|
||
max_field_list: List[str] = []
|
||
for classify in classify_name_list:
|
||
objects = max_map.get(classify) or [0]
|
||
max_field_list.append(_aggregate(objects, "max"))
|
||
|
||
max_len = max(max_counts) if max_counts else 0
|
||
series_data_list: List[List[str]] = []
|
||
for i in range(max_len):
|
||
row_data: List[str] = []
|
||
for category in classify_name_list:
|
||
category_list = classify_map.get(category) or []
|
||
category_data = category_list[i] if i < len(category_list) else []
|
||
row_data.append(category_data[0] if category_data else "")
|
||
series_data_list.append(row_data)
|
||
|
||
result: Dict[str, Any] = {
|
||
"classifyNameField": classify_name_list,
|
||
"seriesDataField": series_data_list,
|
||
}
|
||
if series_name_key:
|
||
result["seriesNameField"] = series_name_list
|
||
if max_key:
|
||
result["maxField"] = max_field_list
|
||
return result
|
||
|
||
|
||
def _echart_configs_from_cells(cells: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
configs: List[Dict[str, Any]] = []
|
||
if not cells:
|
||
return configs
|
||
for key, store in (
|
||
("floatEcharts", cells.get("floatEcharts")),
|
||
("cellEcharts", cells.get("cellEcharts")),
|
||
):
|
||
if not isinstance(store, dict):
|
||
continue
|
||
for drawing_id, item in store.items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
option = item.get("option") or {}
|
||
configs.append(
|
||
{
|
||
"drawingId": item.get("drawingId") or drawing_id,
|
||
"option": option,
|
||
"source": key,
|
||
}
|
||
)
|
||
return configs
|
||
|
||
|
||
def build_chart_data(
|
||
cells: Dict[str, Any],
|
||
datasets: Dict[str, List[Dict[str, Any]]],
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
生成预览用 chartData 列表。
|
||
每项: { drawingId, field: { classifyNameField, seriesNameField, seriesDataField, maxField? } }
|
||
"""
|
||
result: List[Dict[str, Any]] = []
|
||
for cfg in _echart_configs_from_cells(cells):
|
||
drawing_id = cfg.get("drawingId")
|
||
option = cfg.get("option") or {}
|
||
dataset_names: Set[str] = set()
|
||
classify_alias, classify_field = _parse_field(option.get("classifyNameField"))
|
||
series_alias, series_name_field = _parse_field(option.get("seriesNameField"))
|
||
data_alias, series_data_field = _parse_field(option.get("seriesDataField"))
|
||
max_alias, max_field = _parse_field(option.get("maxField"))
|
||
for alias in (classify_alias, series_alias, data_alias, max_alias):
|
||
if alias:
|
||
dataset_names.add(alias)
|
||
if not dataset_names:
|
||
continue
|
||
rows = _collect_rows(datasets, dataset_names)
|
||
field = _build_field(
|
||
rows,
|
||
classify_field,
|
||
series_name_field,
|
||
series_data_field,
|
||
max_field,
|
||
option.get("summaryType") or "none",
|
||
)
|
||
result.append({"drawingId": drawing_id, "field": field})
|
||
return result
|