Files
2026-09-02 11:44:52 +08:00

1435 lines
54 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
宜搭报表图表组件构造 — 按 chart.type 分发,产出 component dict + cube 查询模型。
支持 9 种图表: bar / line / pie / funnel / table / indicator / gauge / combo / pivot
dataSetModelMap 结构严格匹配宜搭报表引擎格式:
- 外层 key: chartData (bar/line/pie/funnel/gauge) / table / youshuData (indicator)
/ dataSetName (combo/pivot)
- 内层: dataViewQueryModel{cubeCode, fieldDefinitionList, fieldList, cubeTenantId}
+ 外层 fieldList (完整字段对象) + xField/yField 等角色字段
"""
from __future__ import annotations
import math
from typing import Any, Optional
from yida_schema_common import (
i18n,
next_node_id,
generate_field_id,
)
# ---------------------------------------------------------------------------
# 图表类型 → componentName 映射
# ---------------------------------------------------------------------------
CHART_TYPE_MAP: dict[str, str] = {
"bar": "YoushuGroupedBarChart",
"line": "YoushuLineChart",
"pie": "YoushuPieChart",
"funnel": "YoushuFunnelChart",
"table": "YoushuTable",
"indicator": "YoushuSimpleIndicatorCard",
"gauge": "YoushuGauge",
"combo": "YoushuComboChart",
"pivot": "YoushuCrossPivotTable",
}
def _compute_dynamic_layout(chart_type: str, chart: dict[str, Any]) -> dict[str, Any]:
"""根据图表内容动态计算布局尺寸。"""
if chart_type == "table":
page_size = chart.get("pageSize", 20)
h = max(16, min(40, round(page_size * 0.9) + 6))
return {"w": 6, "h": h, "minH": h, "maxH": h, "resizeHandles": ["w", "e"]}
if chart_type == "indicator":
kpi_count = len(chart.get("kpi", []))
col = min(kpi_count, 4) or 1
rows = math.ceil(kpi_count / col)
h = max(11, 8 + rows * 5)
w = min(2 * min(kpi_count, 3), 6)
return {"w": w, "h": h, "minH": h, "maxH": h, "resizeHandles": ["w", "e"]}
if chart_type in ("bar", "line"):
y_raw = chart.get("yField", "")
y_count = len(y_raw) if isinstance(y_raw, list) else 1
h = 22 if y_count <= 3 else min(30, 22 + (y_count - 3) * 2)
return {"w": 3, "h": h}
if chart_type == "gauge":
return {"w": 2, "h": 18}
if chart_type == "combo":
return {"w": 6, "h": 22}
if chart_type == "pivot":
return {"w": 6, "h": 30}
return {"w": 3, "h": 22}
_VALUE_SUFFIX_PREFIXES = ("selectField_", "radioField_", "checkboxField_", "multiSelectField_", "employeeField_")
_CUBE_LABEL_SUFFIX: dict[str, str] = {
"selectField_": "_值",
"radioField_": "_值",
"checkboxField_": "_值",
"multiSelectField_": "_值",
"employeeField_": "_名称",
"departmentSelectField_": "_名称",
}
_DS_KEY_CHART = "chartData"
_DS_KEY_TABLE = "table"
_DS_KEY_INDICATOR = "youshuData"
_DS_KEY_DATASET = "dataSetName"
_TIME_GRAN_ID_SUFFIX: dict[str, str] = {
"YEAR": "1", "QUARTER": "2", "MONTH": "3",
"WEEK": "4", "DAY": "5", "HOUR": "6",
}
_TIME_GRAN_FORMAT: dict[str, str] = {
"YEAR": "yyyy", "QUARTER": "yyyy-Q", "MONTH": "yyyy-MM",
"WEEK": "yyyy-w", "DAY": "yyyy-MM-dd", "HOUR": "yyyy-MM-dd HH",
}
_AGGREGATE_TYPES = frozenset({"COUNT", "SUM", "AVG", "MAX", "MIN", "COUNT_DISTINCT"})
# ---------------------------------------------------------------------------
# 公共工具
# ---------------------------------------------------------------------------
def normalize_cube_code(code: str) -> str:
"""FORM-E604838A-3121-43D1-... → FORM_E604838A312143D1..."""
if code.startswith(("FORM-", "FORM_")):
return "FORM_" + code[5:].replace("-", "")
return code.replace("-", "_")
_STRIP_SUFFIXES = ("_id", "_date")
def normalize_field_code(field_code: str) -> str:
fc = field_code
for sfx in _STRIP_SUFFIXES:
if fc.endswith(sfx):
fc = fc[:-len(sfx)]
break
if any(fc.startswith(p) for p in _VALUE_SUFFIX_PREFIXES):
if not fc.endswith("_value"):
return fc + "_value"
return fc
def _apply_cube_label_suffix(field_code: str, text: str) -> str:
"""fieldCode 带 _value 后缀时,同步给显示标签追加中文后缀。
cube 元数据中 selectField_xxx_value 显示名为 "原标签_值"
employeeField_xxx_value 显示名为 "原标签_名称"。
不追加会导致引擎报"找不到配置"。
"""
if not field_code.endswith("_value"):
return text
base = field_code[:-6]
for prefix, suffix in _CUBE_LABEL_SUFFIX.items():
if base.startswith(prefix):
if not text.endswith(suffix):
return text + suffix
return text
return text
def _derive_label(field_code: str) -> str:
"""从 fieldCode 推导可读标签:去前缀、去 _value 后缀。"""
fc = field_code
for p in _VALUE_SUFFIX_PREFIXES:
if fc.startswith(p):
fc = fc[len(p):]
break
else:
for prefix in ("textField_", "numberField_", "dateField_", "textareaField_",
"countrySelectField_", "addressField_", "attachmentField_",
"imageField_", "departmentSelectField_", "tableField_",
"associationFormField_", "serialNumberField_", "rateField_",
"cascadeDateField_"):
if fc.startswith(prefix):
fc = fc[len(prefix):]
break
if fc.endswith("_value"):
fc = fc[:-6]
return fc or field_code
def _infer_data_type(field_code: str) -> str:
if field_code.startswith("numberField_"):
return "NUMBER"
if field_code.startswith("dateField_"):
return "DATE"
if field_code.startswith(("employeeField_", "departmentSelectField_")):
return "STRING" if field_code.endswith("_value") else "ARRAY"
return "STRING"
_ARRAY_FIELD_PREFIXES = ("employeeField_", "departmentSelectField_")
def _fix_array_field_for_aggregate(field_code: str, data_type: str, aggregate_type: str) -> tuple[str, str]:
"""数组字段做聚合时保留 _value 后缀(cube 元数据只认 _value),仅将 ARRAY 类型转为 STRING。"""
if aggregate_type == "NONE":
return field_code, data_type
if any(field_code.startswith(p) for p in _ARRAY_FIELD_PREFIXES):
import warnings
warnings.warn(
f"⚠ {field_code} 是数组字段,不能做聚合({aggregate_type})——"
f"cube SQL 会报 'aggregate cannot contain set-returning function'。"
f"请改用非数组字段(如 numberField / textField)做 {aggregate_type}。",
stacklevel=3,
)
if data_type == "ARRAY":
return field_code, "STRING"
return field_code, data_type
def _is_date_field(field_code: str) -> bool:
return field_code.startswith("dateField_")
def _gen_alias() -> str:
return f"field_{next_node_id()}"
# ---------------------------------------------------------------------------
# 字段解析:支持 string 或 rich field dict
# ---------------------------------------------------------------------------
def _resolve_field(raw: Any, cube_code: str) -> dict[str, Any]:
"""把 string 或 dict 输入统一解析为标准字段元数据。"""
if isinstance(raw, dict):
fc = normalize_field_code(raw.get("fieldCode", ""))
if not fc:
raise ValueError(
f"fieldCode 不能为空。请先通过 `dws yida form components` 获取源表字段定义,"
f"使用返回的 fieldCode 构造字段对象。收到: {raw!r}"
)
text = raw.get("text") or raw.get("label") or _derive_label(fc)
text = _apply_cube_label_suffix(fc, text)
return {
"fieldCode": fc,
"dataType": raw.get("dataType") or _infer_data_type(fc),
"text": text,
"id": raw.get("id") or fc,
"classifiedCode": raw.get("classifiedCode") or cube_code,
"timeGranularityType": raw.get("timeGranularityType"),
"timeFormat": raw.get("timeFormat"),
}
fc = normalize_field_code(str(raw))
if not fc:
raise ValueError(
"fieldCode 不能为空字符串。请先通过 `dws yida form components` 获取源表字段定义。"
)
text = _apply_cube_label_suffix(fc, _derive_label(fc))
return {
"fieldCode": fc,
"dataType": _infer_data_type(fc),
"text": text,
"id": fc,
"classifiedCode": cube_code,
"timeGranularityType": None,
"timeFormat": None,
}
# ---------------------------------------------------------------------------
# 字段对象构造(匹配宜搭报表引擎格式)
# ---------------------------------------------------------------------------
def _build_query_field_def(
alias: str,
label: str,
cube_code: str,
field_code: str,
data_type: str,
aggregate_type: str = "NONE",
time_gran: Optional[str] = None,
classified_code: Optional[str] = None,
) -> dict[str, Any]:
"""dataViewQueryModel.fieldDefinitionList 中的单条定义。"""
return {
"classifiedCode": classified_code or cube_code,
"cubeCode": cube_code,
"fieldCode": field_code,
"dataType": data_type,
"isDim": False,
"aggregateType": aggregate_type,
"alias": alias,
"aliasName": {"type": "i18n", "zh_CN": label, "en_US": label},
"timeGranularityType": time_gran,
}
def _build_full_field(
alias: str,
label: str,
cube_code: str,
field_code: str,
data_type: str,
aggregate_type: str = "NONE",
time_gran: Optional[str] = None,
classified_code: Optional[str] = None,
field_id: Optional[str] = None,
time_format: Optional[str] = None,
) -> dict[str, Any]:
"""外层 fieldList / xField / yField 中的完整字段对象。"""
fid = field_id or field_code
effective_dt = "DOUBLE" if aggregate_type in _AGGREGATE_TYPES else data_type
result: dict[str, Any] = {
"title": i18n(label),
"classifiedCode": classified_code or cube_code,
"cubeCode": cube_code,
"fieldCode": field_code,
"isDimension": "false",
"dataType": effective_dt,
"format": {"type": "NONE"},
"link": [{"type": "NONE"}],
"drillList": [],
"aggregateType": aggregate_type,
"orderBy": {"type": "NONE", "reference": alias},
"fieldKey": alias,
"visible": True,
"beUsedTimes": 1,
"isVisible": "y",
"id": fid,
"text": label,
}
if aggregate_type in _AGGREGATE_TYPES:
result["measureType"] = "MEASURE_ATTRIBUTE"
if time_gran:
result["timeGranularityType"] = time_gran
suffix = _TIME_GRAN_ID_SUFFIX.get(time_gran, "")
if suffix:
result["id"] = field_code + suffix
fmt = time_format or _TIME_GRAN_FORMAT.get(time_gran)
if fmt:
result["timeFormat"] = fmt
return result
def _wrap_dataset(
ds_key: str,
cube_code: str,
cube_tenant_id: str,
query_field_defs: list[dict[str, Any]],
aliases: list[str],
full_fields: list[dict[str, Any]],
extra: Optional[dict[str, Any]] = None,
extra_query: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
"""组装完整 dataSetModelMap(匹配宜搭报表引擎结构)。"""
query_model: dict[str, Any] = {
"cubeCode": cube_code,
"cubeTenantId": cube_tenant_id,
"fieldDefinitionList": query_field_defs,
"fieldList": aliases,
"filterList": [],
"orderByList": [],
}
if extra_query:
query_model.update(extra_query)
ds: dict[str, Any] = {
"cubeCodes": [cube_code],
"dataViewQueryModel": query_model,
"fieldList": full_fields,
"youshuDataType": "real",
"filterList": [],
"limit": "",
"mockData": [],
}
if extra:
ds.update(extra)
return {ds_key: ds}
# ---------------------------------------------------------------------------
# 图表 settings 默认值
# ---------------------------------------------------------------------------
_AXIS_LABEL_STYLE = {
"labelType": "default",
"color": "rgba(23,26,29,0.4)",
"fontSize": 12,
"limitLengthType": "percent",
"percent": 30,
"value": 100,
"autoRotate": True,
"rotate": "0",
"autoHide": True,
}
_X_AXIS_DEFAULT = {
"showXAxis": True,
"showTitle": False,
"title": {"type": "i18n", "zh_CN": "", "en_US": ""},
"line": True,
"tickLine": True,
"grid": False,
"label": True,
"labelStyle": _AXIS_LABEL_STYLE,
"values": {"type": "i18n", "zh_CN": "", "en_US": ""},
}
_Y_AXIS_DEFAULT = {
"showYAxis": True,
"showTitle": False,
"title": {"type": "i18n", "zh_CN": "", "en_US": ""},
"line": False,
"tickLine": False,
"grid": True,
"label": True,
"labelStyle": _AXIS_LABEL_STYLE,
"min": None,
"max": None,
"tickCount": 5,
}
_CUSTOM_COLOR = "#5894FF,#394B76,#F7B900,#E55F24,#80D5F5,#9849B0,#3BC88A,#0E869D,#F4A49E,#80563C"
_LEGEND_DEFAULT = {"showLegend": True, "legendPosition": "top-left", "flipPage": True}
def _get_chart_settings(chart_type: str, chart: dict[str, Any]) -> dict[str, Any]:
if chart_type == "bar":
return {
"container": {"height": 248},
"style": {
"mode": "group",
"linkGroup": False,
"transpose": False,
"barStyle": "ai",
"size": None,
"maxSize": None,
"minSize": None,
"barBackground": None,
"groupSpacing": 0,
"radiusLeftTop": 4,
"radiusRightTop": 4,
"radiusRightBottom": 0,
"radiusLeftBottom": 0,
"colorType": "CUSTOM_COLOR",
"chartColorsMode": "EHRColorsMode",
"customColor": _CUSTOM_COLOR,
},
"countLabel": {"showCountLabel": False, "fontSize": 12, "color": "#000"},
"axisType": "hz",
"xAxis": _X_AXIS_DEFAULT,
"yAxis": _Y_AXIS_DEFAULT,
"legend": _LEGEND_DEFAULT,
"label": {
"showLabel": True,
"labelShowStyle": "ai",
"fontSize": 12,
"autoColor": True,
"color": "#000",
"autoPosition": False,
"position": "middle",
"autoAdjust": True,
"autoHide": True,
},
"slider": {"showSlider": False},
"tooltip": {"showTooltip": True},
}
elif chart_type == "line":
return {
"container": {"height": 248},
"style": {
"mode": "none",
"lineStyle": "ai",
"lineSize": 2,
"smooth": False,
"showArea": False,
"areaOpacity": 0.25,
"showPoint": True,
"pointSize": 4,
"pointShape": "circle",
"showLine": True,
"lineWidth": 2,
"colorType": "CUSTOM_COLOR",
"chartColorsMode": "EHRColorsMode",
"customColor": _CUSTOM_COLOR,
},
"axisType": "hz",
"xAxis": _X_AXIS_DEFAULT,
"yAxis": _Y_AXIS_DEFAULT,
"legend": _LEGEND_DEFAULT,
"label": {"showLabel": True, "fontSize": 12, "color": "#000", "autoOverlap": True},
"slider": {"showSlider": False},
"tooltip": {"showTooltip": True},
}
elif chart_type == "pie":
return {
"container": {"height": 248},
"style": {
"radius": 75,
"isRing": False,
"innerRadius": 0,
"colorType": "CUSTOM_COLOR",
"chartColorsMode": "EHRColorsMode",
"customColor": _CUSTOM_COLOR,
},
"statistic": {"showStatistic": False},
"label": {
"showLabel": True,
"showLine": True,
"labelAlign": "outer",
"labelSize": 12,
"labelColor": "#404040",
"labelFormatType": "NAME_PERCENT",
},
"legend": {
"showLegend": True,
"legendPosition": "right",
"flipPage": True,
"type": "item",
"contentType": "NAME",
"cardWidth": None,
"ratio": 65,
"layout": "vertical",
"itemSpacing": 12,
},
"tooltip": {"showTooltip": True, "contentType": None},
"percentDigits": 2,
}
elif chart_type == "funnel":
return {
"container": {"height": 248},
"style": {
"colorType": "CUSTOM_COLOR",
"chartColorsMode": "EHRColorsMode",
"customColor": _CUSTOM_COLOR,
},
"legend": _LEGEND_DEFAULT,
"label": {"showLabel": True, "fontSize": 12, "color": "#000"},
"tooltip": {"showTooltip": True},
}
elif chart_type == "gauge":
return {
"container": {"height": 248},
"useSingleColor": False,
"singleColor": "#0089FF",
"color": [],
"tick": {"showTick": True, "min": None, "max": None, "tickInterval": None},
"assistValue": {"openAssistValue": True, "showCompare": False, "position": "bottom"},
"style": {"rounded": True, "pivot": True, "rangeSize": 16, "radius": 95, "innerRadius": 90},
}
elif chart_type == "combo":
return {
"container": {"height": 248},
"style": {
"sync": False,
"chartType": "bar-line",
"bar": {
"size": None, "maxSize": None, "minSize": None, "mode": "group",
"barBackground": None,
"radiusLeftTop": 4, "radiusRightTop": 4,
"radiusRightBottom": 0, "radiusLeftBottom": 0,
},
"line": {
"size": 2, "smooth": False,
"showPoint": True, "pointSize": 4, "pointShape": "circle",
},
"autoAdjust": True,
"colorType": "CUSTOM_COLOR",
"chartColorsMode": "EHRColorsMode",
"customColor": _CUSTOM_COLOR,
},
"xAxis": _X_AXIS_DEFAULT,
"leftYAxis": {
"showLeftYAxis": True, "showTitle": False,
"title": {"type": "i18n", "zh_CN": "", "en_US": ""},
"line": False, "tickLine": False, "grid": True, "label": True,
"labelStyle": _AXIS_LABEL_STYLE,
"min": None, "max": None, "tickCount": 5,
},
"rightYAxis": {
"showRightYAxis": True, "showTitle": False,
"title": {"type": "i18n", "zh_CN": "", "en_US": ""},
"line": False, "tickLine": False, "grid": False, "label": True,
"labelStyle": _AXIS_LABEL_STYLE,
"min": None, "max": None, "tickCount": 5,
},
"legend": _LEGEND_DEFAULT,
"leftLabel": {"showLabel": True, "fontSize": 12, "color": "#000"},
"rightLabel": {"showLabel": True, "fontSize": 12, "color": "#000"},
"slider": {"showSlider": False},
"tooltip": {"showTooltip": True},
}
elif chart_type == "table":
page_size = chart.get("pageSize", 20)
return {
"rglConfig": {"w": 6, "h": 21, "isHeightAuto": True},
"size": "medium",
"wordSize": "medium:14",
"theme": "split",
"mergeCell": False,
"fixedHeader": False,
"maxBodyHeight": "300",
"fixedColumnIndex": 1,
"isReverseTable": False,
"showReversedHeader": False,
"isUniqueRows": False,
"pagination": {
"isPagination": True,
"pageSize": page_size,
"pageShowCount": 5,
"showPageSelect": False,
"size": "small",
"type": "normal",
},
"isTree": False,
"idField": None,
"pidField": None,
"isLeaf": None,
"drilldownFilterList": None,
"defaultExpand": False,
"rankStyle": False,
"container": {"height": 472},
"titleTip": False,
"showCopyData": False,
"enableFieldSelect": False,
"defaultSelectedFields": "",
"hasFullscreen": False,
"copyAsImg": False,
"height": None,
"isHeightAuto": True,
}
elif chart_type == "indicator":
kpi_count = len(chart.get("kpi", []))
return {
"showSideStyle": "NONE",
"followTheme": False,
"themeType": "dark",
"showSideBorder": True,
"sideBarColor": "#0089FF",
"bgColorType": "single",
"singleBgColor": "#F1F2F3",
"colorType": "SCHEMA_COLOR",
"multipleBgColor": "defaultColorsMode",
"customColor": "#0089FF,#FF9200,#11AB4F,#FFD100,#7263EE,#67C5EB,#6B748C,#FF755A,#007E99,#FFA8A8",
"size": "normal",
"valueSize": "20px",
"titleMaxRow": 0,
"columnCount": min(kpi_count, 4) or 4,
"columnCountForH5": min(kpi_count, 2) or 2,
"popoverAlign": "b",
"container": {"height": 72},
"titleTip": False,
"enableFieldSelect": False,
"hasFullscreen": False,
"copyAsImg": False,
"height": None,
"isHeightAuto": True,
}
elif chart_type == "pivot":
return {
"rglConfig": {"w": 6, "h": 21, "isHeightAuto": True},
"maxBodyHeight": 500,
"size": "normal",
"rows": [],
"columns": [],
"measures": [],
"details": [],
"supportExport": False,
"exportType": "XJZ",
"dialogWidth": 850,
"dialogPageSize": 10,
"baseInfo": {
"isShowSetter": True, "isShowFilter": False, "isShowReload": False,
"isHideTitle": False, "isMeasureOrder": True, "isZebra": True,
"rowMaxSize": 3000, "columnsMaxSize": 500, "columnWidth": 100,
"dialogWidth": 850, "dialogPageSize": 10,
"detailExportData": {"supportExport": False, "exportType": "BROWSER"},
},
"mode": "summary",
"summaryInfo": {
"isRowTotal": True, "rowTotalWidth": 130, "rowTotalPosition": "end",
"isColumnTotal": True, "isSubTotal": False,
"rowMaxSize": 3000, "columnsMaxSize": 500,
},
"paginationInfo": {
"size": "small", "type": "normal", "pageShowCount": 5,
"pageSize": 10, "showPageSelect": False,
},
"container": {"height": 232},
"titleTip": False,
"hasFullscreen": False,
"copyAsImg": False,
"height": None,
"isHeightAuto": True,
}
return {}
# ---------------------------------------------------------------------------
# userConfig 构建(设计器数据配置面板)
# ---------------------------------------------------------------------------
def _setter(name: str, title: str, **extra_props: Any) -> dict[str, Any]:
item: dict[str, Any] = {"setterName": "ColumnFieldSetter", "name": name, "title": title}
if extra_props:
item["setterProps"] = extra_props
return item
def _build_user_config(chart_type: str) -> list[dict[str, Any]]:
if chart_type in ("bar", "line"):
return [{"name": _DS_KEY_CHART, "title": "配置数据", "items": [
_setter("xField", "横轴", single=True, showFormatTab=True, showFormulaEditor=True,
showFieldInfo=True, showAggregateTab=False, showDrillTab=True, showEditTab=True, showSortTab=True),
_setter("yField", "纵轴", showFormatTab=True, showFormulaEditor=True,
showFieldInfo=True, showEditTab=True, showSortTab=True, showDataLink=True),
_setter("groupField", "分组", single=True, showFormatTab=True, showEditTab=True),
_setter("annotationField", "参考线", showFormatTab=True, showEditTab=True),
]}]
if chart_type == "pie":
return [{"name": _DS_KEY_CHART, "title": "配置数据", "items": [
_setter("xField", "分类字段", single=True, showAggregateTab=False, showDrillTab=True, showColorTab=True),
_setter("yField", "数值字段", single=True, showDataLink=True),
_setter("ratio", "趋势值字段"),
_setter("totalValue", "总值字段"),
_setter("totalRatio", "总趋势值字段"),
]}]
if chart_type == "funnel":
return [{"name": _DS_KEY_CHART, "title": "配置数据", "items": [
_setter("xField", "横轴", single=True, showFormatTab=True, showAggregateTab=False,
showDrillTab=True, showEditTab=True, showSortTab=True),
_setter("yField", "纵轴", showFormatTab=True, showEditTab=True, showSortTab=True, showDataLink=True),
]}]
if chart_type == "combo":
return [{"name": _DS_KEY_DATASET, "title": "配置数据", "items": [
_setter("xField", "横轴", single=True),
_setter("leftYFields", "左纵轴", showDataLink=True),
_setter("rightYFields", "右纵轴", showDataLink=True),
_setter("annotationField", "参考线"),
]}]
if chart_type == "table":
return [{"name": _DS_KEY_TABLE, "title": "配置数据", "items": [
_setter("columnFields", "列", showFormulaEditor=True, showFieldInfo=True,
showDataLink=True, supportDynamicAlias=True, showBatchSet=True,
batchSetFields=["text", "title", "aggregateType", "format_type", "format_decimalDigit"]),
]}]
if chart_type == "indicator":
return [{"name": _DS_KEY_INDICATOR, "title": "指标数据", "items": [
_setter("kpi", "指标", single=False, showDataLink=True, supportDynamicAlias=True,
showBatchSet=True,
batchSetFields=["text", "title", "titleTip", "aggregateType",
"format_type", "format_decimalDigit", "unit"]),
_setter("helpKpi", "辅助指标", single=False, showDataLink=True),
]}]
if chart_type == "pivot":
return [{"name": _DS_KEY_DATASET, "title": "配置数据", "items": [
_setter("columnList", "列", showFormatTab=True, showEditTab=True),
]}]
if chart_type == "gauge":
return [{"name": _DS_KEY_CHART, "title": "配置数据", "items": [
_setter("valueField", "指标值", single=True),
_setter("assitValueField", "辅助值", single=True),
]}]
return []
# ---------------------------------------------------------------------------
# mockData 构建(设计器预览数据)
# ---------------------------------------------------------------------------
def _build_mock_data(chart_type: str) -> list[dict[str, Any]]:
if chart_type == "bar":
return [{"name": _DS_KEY_CHART, "data": {"data": [
{"month": "Jan.", "value": 18.9}, {"month": "Feb.", "value": 28.8},
{"month": "Mar.", "value": 39.3}, {"month": "Apr.", "value": 81.4},
{"month": "May", "value": 47},
], "meta": [
{"fieldKey": "xField", "dataType": "STRING", "title": "month"},
{"fieldKey": "yField", "dataType": "NUMBER", "title": "value"},
], "currentPage": 1, "totalCount": 5}}]
if chart_type == "line":
return [{"name": _DS_KEY_CHART, "data": {"data": [
{"xField": "2020", "yField": 3}, {"xField": "2021", "yField": 4},
{"xField": "2022", "yField": 3.5}, {"xField": "2023", "yField": 5},
{"xField": "2024", "yField": 4.9},
], "meta": [
{"fieldKey": "xField", "dataType": "STRING", "title": "xField"},
{"fieldKey": "yField", "dataType": "NUMBER", "title": "yField"},
], "currentPage": 1, "totalCount": 5}}]
if chart_type == "pie":
return [{"name": _DS_KEY_CHART, "data": {"data": [
{"xField": "分类A", "yField": 63, "ratio": 0.8, "totalValue": 202, "totalRatio": 0.32},
{"xField": "分类B", "yField": 72, "ratio": 0.5, "totalValue": 202, "totalRatio": 0.36},
{"xField": "分类C", "yField": 67, "ratio": 0.3, "totalValue": 202, "totalRatio": 0.33},
], "meta": [
{"fieldKey": "xField", "dataType": "STRING", "title": "xField"},
{"fieldKey": "yField", "dataType": "NUMBER", "title": "yField"},
{"fieldKey": "ratio", "dataType": "NUMBER", "title": "ratio"},
{"fieldKey": "totalValue", "dataType": "NUMBER", "title": "totalValue"},
{"fieldKey": "totalRatio", "dataType": "NUMBER", "title": "totalRatio"},
], "currentPage": 1, "totalCount": 3}}]
if chart_type == "funnel":
return [{"name": _DS_KEY_CHART, "data": {"data": [
{"xField": "展示", "yField": 100}, {"xField": "点击", "yField": 80},
{"xField": "访问", "yField": 60}, {"xField": "咨询", "yField": 40},
{"xField": "订单", "yField": 20},
], "meta": [
{"fieldKey": "xField", "dataType": "STRING", "title": "xField"},
{"fieldKey": "yField", "dataType": "NUMBER", "title": "yField"},
], "currentPage": 1, "totalCount": 5}}]
if chart_type == "gauge":
return [{"name": _DS_KEY_CHART, "data": {"data": [
{"value": 75, "assitValue": 100},
], "meta": [
{"fieldKey": "valueField", "dataType": "NUMBER", "title": "value"},
{"fieldKey": "assitValueField", "dataType": "NUMBER", "title": "assitValue"},
], "currentPage": 1, "totalCount": 1}}]
if chart_type == "combo":
return [{"name": _DS_KEY_DATASET, "data": {"data": [
{"xField": "Jan.", "leftY": 18.9, "rightY": 5},
{"xField": "Feb.", "leftY": 28.8, "rightY": 8},
{"xField": "Mar.", "leftY": 39.3, "rightY": 12},
{"xField": "Apr.", "leftY": 81.4, "rightY": 15},
{"xField": "May", "leftY": 47, "rightY": 10},
], "meta": [
{"fieldKey": "xField", "dataType": "STRING", "title": "xField"},
{"fieldKey": "leftYFields", "dataType": "NUMBER", "title": "leftY"},
{"fieldKey": "rightYFields", "dataType": "NUMBER", "title": "rightY"},
], "currentPage": 1, "totalCount": 5}}]
if chart_type == "table":
return [{"name": _DS_KEY_TABLE, "data": {"data": [
{"col1": "数据1", "col2": "数据2", "col3": 100},
{"col1": "数据4", "col2": "数据5", "col3": 200},
{"col1": "数据7", "col2": "数据8", "col3": 300},
], "meta": [
{"fieldKey": "columnFields", "dataType": "STRING", "title": "col1"},
{"fieldKey": "columnFields", "dataType": "STRING", "title": "col2"},
{"fieldKey": "columnFields", "dataType": "NUMBER", "title": "col3"},
], "currentPage": 1, "totalCount": 3}}]
if chart_type == "indicator":
return [{"name": _DS_KEY_INDICATOR, "data": {"data": [
{"kpi1": 23123, "kpi2": 7712},
], "meta": [
{"fieldKey": "kpi1", "dataType": "NUMBER", "title": "指标1", "category": "kpi"},
{"fieldKey": "kpi2", "dataType": "NUMBER", "title": "指标2", "category": "kpi"},
], "currentPage": 1, "totalCount": 1}}]
if chart_type == "pivot":
return [{"name": _DS_KEY_DATASET, "data": {"data": [
{"col1": 74, "col2": 9, "col3": 79},
{"col1": 85, "col2": 15, "col3": 62},
{"col1": 93, "col2": 28, "col3": 44},
], "meta": [
{"fieldKey": "columnList", "dataType": "NUMBER", "title": "col1"},
{"fieldKey": "columnList", "dataType": "NUMBER", "title": "col2"},
{"fieldKey": "columnList", "dataType": "NUMBER", "title": "col3"},
], "currentPage": 1, "totalCount": 3}}]
return []
# ---------------------------------------------------------------------------
# afterFetch / exportData / link 构建
# ---------------------------------------------------------------------------
_AFTER_FETCH = {
"type": "JSFunction",
"value": "function afterFetch(data, extraInfo) { return data; }",
}
_EXPORT_DATA = {
"supportExport": False,
"passType": "NO_PASS",
"exportType": "BROWSER",
}
_LINK = {
"hasLink": False,
"content": {"type": "i18n", "zh_CN": "更多", "en_US": "More"},
"onlyIcon": True,
}
# ---------------------------------------------------------------------------
# 顶层字段属性提取
# ---------------------------------------------------------------------------
def _extract_top_level_field_props(chart_type: str, ds_model_map: dict[str, Any]) -> dict[str, Any]:
"""从 dataSetModelMap 提取需要写入 props 顶层的字段属性。"""
result: dict[str, Any] = {}
if chart_type in ("bar", "line", "pie", "funnel"):
ds = ds_model_map.get(_DS_KEY_CHART, {})
result["xField"] = ds.get("xField", [])
result["yField"] = ds.get("yField", [])
if chart_type not in ("pie", "funnel"):
result["groupField"] = ds.get("groupField", [])
elif chart_type == "gauge":
ds = ds_model_map.get(_DS_KEY_CHART, {})
result["valueField"] = ds.get("valueField", [])
result["assitValueField"] = ds.get("assitValueField", [])
elif chart_type == "indicator":
ds = ds_model_map.get(_DS_KEY_INDICATOR, {})
result["kpiField"] = ds.get("kpi", [])
result["helpKpiField"] = ds.get("helpKpi", [])
elif chart_type == "table":
ds = ds_model_map.get(_DS_KEY_TABLE, {})
result["columnField"] = ds.get("columnFields", [])
elif chart_type == "combo":
ds = ds_model_map.get(_DS_KEY_DATASET, {})
result["xField"] = ds.get("xField", [])
result["leftYFields"] = ds.get("leftYFields", [])
result["rightYFields"] = ds.get("rightYFields", [])
elif chart_type == "pivot":
ds = ds_model_map.get(_DS_KEY_DATASET, {})
result["columnList"] = ds.get("columnList", [])
return result
# ---------------------------------------------------------------------------
# 主入口
# ---------------------------------------------------------------------------
def build_chart_component(
chart: dict[str, Any],
cube_tenant_id: str = "",
) -> tuple[dict[str, Any], str, dict[str, Any]]:
"""
构造图表组件。
Returns:
(component_node, fieldId, default_layout)
"""
chart_type = chart.get("type", "bar")
if chart_type not in CHART_TYPE_MAP:
raise ValueError(f"不支持的图表类型: {chart_type}(支持: {list(CHART_TYPE_MAP.keys())}")
component_name = CHART_TYPE_MAP[chart_type]
title = chart.get("title", "图表")
field_id = generate_field_id(component_name)
node_id = next_node_id()
cube_code = normalize_cube_code(chart.get("cubeCode", ""))
if not cube_code:
raise ValueError(f"图表 '{title}' 缺少 cubeCode")
data_set_model_map = _build_data_set_model_map(chart, cube_code, cube_tenant_id)
props: dict[str, Any] = {
"fieldId": field_id,
"cid": node_id,
"showComponentTitle": True,
"componentTitle": i18n(title),
"componentTitleTextAlign": "LEFT",
"titleTipContent": i18n(""),
"titleTipIconName": "help",
"headerSize": "medium",
"link": _LINK,
"exportData": _EXPORT_DATA,
"openRefresh": True,
"enabledCache": True,
"auth": [],
"afterFetch": _AFTER_FETCH,
"__style__": {},
"mockData": _build_mock_data(chart_type),
"dataSetModelMap": data_set_model_map,
"userConfig": _build_user_config(chart_type),
"settings": _get_chart_settings(chart_type, chart),
"titleTip": False,
"hasFullscreen": False,
"copyAsImg": False,
"height": None,
"isHeightAuto": True,
"datasetModel": {"filterList": []},
}
top_level = _extract_top_level_field_props(chart_type, data_set_model_map)
props.update(top_level)
if chart_type in ("table", "indicator"):
props["showFieldSelectIcon"] = True
if chart_type == "table":
props["pageSize"] = chart.get("pageSize", 20)
node: dict[str, Any] = {
"componentName": component_name,
"id": node_id,
"props": props,
}
computed = _compute_dynamic_layout(chart_type, chart)
layout: dict[str, Any] = {
"w": chart.get("w", computed["w"]),
"h": chart.get("h", computed["h"]),
}
for k in ("minH", "maxH", "resizeHandles"):
if k in computed:
layout[k] = computed[k]
return node, field_id, layout
# ---------------------------------------------------------------------------
# dataSetModelMap 构造
# ---------------------------------------------------------------------------
def _build_data_set_model_map(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
chart_type = chart.get("type", "bar")
if chart_type in ("bar", "line", "pie", "funnel"):
return _build_xy_data_model(chart, cube_code, cube_tenant_id)
elif chart_type == "table":
return _build_table_data_model(chart, cube_code, cube_tenant_id)
elif chart_type == "indicator":
return _build_indicator_data_model(chart, cube_code, cube_tenant_id)
elif chart_type == "gauge":
return _build_gauge_data_model(chart, cube_code, cube_tenant_id)
elif chart_type == "combo":
return _build_combo_data_model(chart, cube_code, cube_tenant_id)
elif chart_type == "pivot":
return _build_pivot_data_model(chart, cube_code, cube_tenant_id)
return {}
def _build_xy_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
chart_type = chart.get("type", "bar")
x_raw = chart.get("xField", "")
x_resolved = _resolve_field(x_raw, cube_code)
y_raw = chart.get("yField", "")
if isinstance(y_raw, (str, dict)):
y_raw = [y_raw]
y_resolved_list = [_resolve_field(f, cube_code) for f in y_raw]
agg_type = chart.get("aggregateType", "COUNT").upper()
x_label = _apply_cube_label_suffix(
x_resolved["fieldCode"], chart.get("xLabel", "") or x_resolved["text"]
)
y_labels_raw = chart.get("yLabel", [])
if isinstance(y_labels_raw, str):
y_labels_raw = [y_labels_raw]
x_alias = _gen_alias()
x_time_gran = x_resolved["timeGranularityType"] or (
chart.get("timeGranularityType", "DAY") if _is_date_field(x_resolved["fieldCode"]) else None
)
query_defs = [_build_query_field_def(
x_alias, x_label, cube_code, x_resolved["fieldCode"], x_resolved["dataType"],
"NONE", x_time_gran, classified_code=x_resolved["classifiedCode"],
)]
x_full = _build_full_field(
x_alias, x_label, cube_code, x_resolved["fieldCode"], x_resolved["dataType"],
"NONE", x_time_gran, classified_code=x_resolved["classifiedCode"],
field_id=x_resolved["id"], time_format=x_resolved["timeFormat"],
)
all_full = [x_full]
y_aliases: list[str] = []
y_fulls: list[dict[str, Any]] = []
for i, yr in enumerate(y_resolved_list):
y_label = _apply_cube_label_suffix(
yr["fieldCode"], (y_labels_raw[i] if i < len(y_labels_raw) else "") or yr["text"]
)
y_alias = _gen_alias()
y_aliases.append(y_alias)
y_fc, y_dt = _fix_array_field_for_aggregate(yr["fieldCode"], yr["dataType"], agg_type)
query_defs.append(_build_query_field_def(
y_alias, y_label, cube_code, y_fc, y_dt,
agg_type, classified_code=yr["classifiedCode"],
))
y_full = _build_full_field(
y_alias, y_label, cube_code, y_fc, y_dt,
agg_type, classified_code=yr["classifiedCode"], field_id=yr["id"],
)
y_fulls.append(y_full)
all_full.append(y_full)
extra: dict[str, Any] = {"xField": [x_full], "yField": y_fulls}
if chart_type == "pie":
extra.update({"ratio": [], "totalValue": [], "totalRatio": [], "trailingIconField": []})
elif chart_type != "funnel":
extra.update({"groupField": [], "annotationField": []})
limit_val = chart.get("limit")
if limit_val is not None:
extra["limit"] = int(limit_val)
return _wrap_dataset(
_DS_KEY_CHART, cube_code, cube_tenant_id,
query_defs, [x_alias] + y_aliases, all_full,
extra=extra,
)
def _build_table_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
raw_columns = chart.get("columnFields", []) or chart.get("columns", []) or chart.get("fields", [])
if not raw_columns:
x_field = chart.get("xField", "")
if x_field:
raw_columns = [x_field]
if not raw_columns:
raise ValueError(
"table 图表必须提供 columnFields(或 columns / fields / xField)。"
"请先通过 `dws yida form components` 获取源表字段定义。"
)
column_labels_raw = chart.get("columnLabels", [])
resolved_columns = [_resolve_field(col, cube_code) for col in raw_columns]
query_defs: list[dict[str, Any]] = []
aliases: list[str] = []
full_fields: list[dict[str, Any]] = []
for i, rc in enumerate(resolved_columns):
label = _apply_cube_label_suffix(
rc["fieldCode"], (column_labels_raw[i] if i < len(column_labels_raw) else "") or rc["text"]
)
alias = _gen_alias()
aliases.append(alias)
time_gran = rc["timeGranularityType"] or (
chart.get("timeGranularityType", "DAY") if _is_date_field(rc["fieldCode"]) else None
)
query_defs.append(_build_query_field_def(
alias, label, cube_code, rc["fieldCode"], rc["dataType"],
"NONE", time_gran, classified_code=rc["classifiedCode"],
))
full = _build_full_field(
alias, label, cube_code, rc["fieldCode"], rc["dataType"],
"NONE", time_gran, classified_code=rc["classifiedCode"],
field_id=rc["id"], time_format=rc["timeFormat"],
)
full["sortable"] = False
full["hidden"] = False
full_fields.append(full)
column_fields: list[dict[str, Any]] = []
for i, full in enumerate(full_fields):
col: dict[str, Any] = {
"aggregateType": full["aggregateType"],
"beUsedTimes": 1,
"classifiedCode": full["classifiedCode"],
"cubeCode": full["cubeCode"],
"dataType": full["dataType"],
"fieldCode": full["fieldCode"],
"fieldKey": full["fieldKey"],
"id": full["id"],
"text": full["text"],
"title": full["title"],
"visible": True,
"width": 120,
"align": "left",
"fixed": "left" if i == 0 else None,
"timeGranularityType": full.get("timeGranularityType"),
"timeFormat": full.get("timeFormat"),
"orderBy": full["orderBy"],
}
column_fields.append(col)
return _wrap_dataset(
_DS_KEY_TABLE, cube_code, cube_tenant_id,
query_defs, aliases, full_fields,
extra={"columnFields": column_fields},
)
def _build_indicator_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
kpi_list = chart.get("kpi", []) or chart.get("kpiField", []) or chart.get("yField", []) or chart.get("fields", [])
if not kpi_list:
raise ValueError("indicator 图表必须提供 kpi(或 kpiField / yField / fields)数组")
query_defs: list[dict[str, Any]] = []
aliases: list[str] = []
full_fields: list[dict[str, Any]] = []
kpi_rich: list[dict[str, Any]] = []
for kpi in kpi_list:
if isinstance(kpi, str):
kpi = {"fieldCode": kpi}
fc = normalize_field_code(kpi.get("fieldCode", ""))
agg = kpi.get("aggregateType", "COUNT").upper()
dt = kpi.get("dataType") or _infer_data_type(fc)
fc, dt = _fix_array_field_for_aggregate(fc, dt, agg)
label = kpi.get("aliasName", "") or kpi.get("text", "") or kpi.get("label", "") or _derive_label(fc)
label = _apply_cube_label_suffix(fc, label)
field_id = kpi.get("id") or fc
classified_code = kpi.get("classifiedCode") or cube_code
alias = _gen_alias()
aliases.append(alias)
query_defs.append(_build_query_field_def(
alias, label, cube_code, fc, dt, agg, classified_code=classified_code,
))
full = _build_full_field(
alias, label, cube_code, fc, dt, agg,
classified_code=classified_code, field_id=field_id,
)
full_fields.append(full)
kpi_item: dict[str, Any] = {
**full,
"visible": True,
"isVisible": "y",
}
kpi_rich.append(kpi_item)
help_kpi_list = chart.get("helpKpi", [])
help_kpi_rich: list[dict[str, Any]] = []
for hk in help_kpi_list:
if isinstance(hk, str):
hk = {"fieldCode": hk}
fc = normalize_field_code(hk.get("fieldCode", ""))
agg = hk.get("aggregateType", "COUNT").upper()
dt = hk.get("dataType") or _infer_data_type(fc)
fc, dt = _fix_array_field_for_aggregate(fc, dt, agg)
label = hk.get("aliasName", "") or hk.get("text", "") or hk.get("label", "") or _derive_label(fc)
label = _apply_cube_label_suffix(fc, label)
field_id = hk.get("id") or fc
classified_code = hk.get("classifiedCode") or cube_code
alias = _gen_alias()
aliases.append(alias)
query_defs.append(_build_query_field_def(
alias, label, cube_code, fc, dt, agg, classified_code=classified_code,
))
full = _build_full_field(
alias, label, cube_code, fc, dt, agg,
classified_code=classified_code, field_id=field_id,
)
full_fields.append(full)
help_kpi_rich.append({**full, "visible": True, "isVisible": "y"})
return _wrap_dataset(
_DS_KEY_INDICATOR, cube_code, cube_tenant_id,
query_defs, aliases, full_fields,
extra={"kpi": kpi_rich, "helpKpi": help_kpi_rich},
)
def _build_gauge_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
value_raw = chart.get("valueField") or chart.get("yField")
if isinstance(value_raw, list):
value_raw = value_raw[0] if value_raw else None
if not value_raw:
raise ValueError("gauge 图表必须提供 valueField(或 yField")
value_resolved = _resolve_field(value_raw, cube_code)
agg_type = chart.get("aggregateType", "AVG").upper()
v_alias = _gen_alias()
v_label = _apply_cube_label_suffix(
value_resolved["fieldCode"], chart.get("valueLabel", "") or value_resolved["text"]
)
v_fc, v_dt = _fix_array_field_for_aggregate(value_resolved["fieldCode"], value_resolved["dataType"], agg_type)
query_defs = [_build_query_field_def(
v_alias, v_label, cube_code, v_fc, v_dt, agg_type,
classified_code=value_resolved["classifiedCode"],
)]
v_full = _build_full_field(
v_alias, v_label, cube_code, v_fc, v_dt, agg_type,
classified_code=value_resolved["classifiedCode"], field_id=value_resolved["id"],
)
all_full = [v_full]
aliases = [v_alias]
assit_fulls: list[dict[str, Any]] = []
assit_raw = chart.get("assitValueField")
if assit_raw:
if isinstance(assit_raw, list):
assit_raw = assit_raw[0] if assit_raw else None
if assit_raw:
assit_resolved = _resolve_field(assit_raw, cube_code)
a_alias = _gen_alias()
a_label = _apply_cube_label_suffix(
assit_resolved["fieldCode"], chart.get("assitLabel", "") or assit_resolved["text"]
)
a_fc, a_dt = _fix_array_field_for_aggregate(
assit_resolved["fieldCode"], assit_resolved["dataType"], agg_type
)
query_defs.append(_build_query_field_def(
a_alias, a_label, cube_code, a_fc, a_dt, agg_type,
classified_code=assit_resolved["classifiedCode"],
))
a_full = _build_full_field(
a_alias, a_label, cube_code, a_fc, a_dt, agg_type,
classified_code=assit_resolved["classifiedCode"], field_id=assit_resolved["id"],
)
all_full.append(a_full)
aliases.append(a_alias)
assit_fulls.append(a_full)
return _wrap_dataset(
_DS_KEY_CHART, cube_code, cube_tenant_id,
query_defs, aliases, all_full,
extra={"valueField": [v_full], "assitValueField": assit_fulls},
)
def _build_combo_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
x_raw = chart.get("xField", "")
x_resolved = _resolve_field(x_raw, cube_code)
left_raw = chart.get("leftYFields", [])
if isinstance(left_raw, (str, dict)):
left_raw = [left_raw]
right_raw = chart.get("rightYFields", [])
if isinstance(right_raw, (str, dict)):
right_raw = [right_raw]
if not left_raw and not right_raw:
raise ValueError("combo 图表必须提供 leftYFields 或 rightYFields")
left_resolved = [_resolve_field(f, cube_code) for f in left_raw]
right_resolved = [_resolve_field(f, cube_code) for f in right_raw]
left_agg = chart.get("leftAggregateType", "SUM").upper()
right_agg = chart.get("rightAggregateType", "SUM").upper()
x_label = _apply_cube_label_suffix(
x_resolved["fieldCode"], chart.get("xLabel", "") or x_resolved["text"]
)
x_alias = _gen_alias()
x_time_gran = x_resolved["timeGranularityType"] or (
chart.get("timeGranularityType", "DAY") if _is_date_field(x_resolved["fieldCode"]) else None
)
query_defs = [_build_query_field_def(
x_alias, x_label, cube_code, x_resolved["fieldCode"], x_resolved["dataType"],
"NONE", x_time_gran, classified_code=x_resolved["classifiedCode"],
)]
x_full = _build_full_field(
x_alias, x_label, cube_code, x_resolved["fieldCode"], x_resolved["dataType"],
"NONE", x_time_gran, classified_code=x_resolved["classifiedCode"],
field_id=x_resolved["id"], time_format=x_resolved["timeFormat"],
)
all_full = [x_full]
all_aliases = [x_alias]
left_fulls: list[dict[str, Any]] = []
for lr in left_resolved:
alias = _gen_alias()
all_aliases.append(alias)
label = _apply_cube_label_suffix(lr["fieldCode"], lr["text"])
fc, dt = _fix_array_field_for_aggregate(lr["fieldCode"], lr["dataType"], left_agg)
query_defs.append(_build_query_field_def(
alias, label, cube_code, fc, dt, left_agg, classified_code=lr["classifiedCode"],
))
full = _build_full_field(
alias, label, cube_code, fc, dt, left_agg,
classified_code=lr["classifiedCode"], field_id=lr["id"],
)
left_fulls.append(full)
all_full.append(full)
right_fulls: list[dict[str, Any]] = []
for rr in right_resolved:
alias = _gen_alias()
all_aliases.append(alias)
label = _apply_cube_label_suffix(rr["fieldCode"], rr["text"])
fc, dt = _fix_array_field_for_aggregate(rr["fieldCode"], rr["dataType"], right_agg)
query_defs.append(_build_query_field_def(
alias, label, cube_code, fc, dt, right_agg, classified_code=rr["classifiedCode"],
))
full = _build_full_field(
alias, label, cube_code, fc, dt, right_agg,
classified_code=rr["classifiedCode"], field_id=rr["id"],
)
right_fulls.append(full)
all_full.append(full)
return _wrap_dataset(
_DS_KEY_DATASET, cube_code, cube_tenant_id,
query_defs, all_aliases, all_full,
extra={
"xField": [x_full],
"leftYFields": left_fulls,
"rightYFields": right_fulls,
"annotationField": [],
},
)
def _build_pivot_data_model(
chart: dict[str, Any],
cube_code: str,
cube_tenant_id: str,
) -> dict[str, Any]:
raw_columns = chart.get("columnList", []) or chart.get("columns", [])
if not raw_columns:
raise ValueError(
"pivot 图表必须提供 columnList(或 columns)。"
"请先通过 `dws yida form components` 获取源表字段定义。"
)
resolved_columns = [_resolve_field(col, cube_code) for col in raw_columns]
query_defs: list[dict[str, Any]] = []
aliases: list[str] = []
full_fields: list[dict[str, Any]] = []
for rc in resolved_columns:
label = _apply_cube_label_suffix(rc["fieldCode"], rc["text"])
alias = _gen_alias()
aliases.append(alias)
time_gran = rc["timeGranularityType"] or (
chart.get("timeGranularityType", "DAY") if _is_date_field(rc["fieldCode"]) else None
)
query_defs.append(_build_query_field_def(
alias, label, cube_code, rc["fieldCode"], rc["dataType"],
"NONE", time_gran, classified_code=rc["classifiedCode"],
))
full = _build_full_field(
alias, label, cube_code, rc["fieldCode"], rc["dataType"],
"NONE", time_gran, classified_code=rc["classifiedCode"],
field_id=rc["id"], time_format=rc["timeFormat"],
)
full_fields.append(full)
return _wrap_dataset(
_DS_KEY_DATASET, cube_code, cube_tenant_id,
query_defs, aliases, full_fields,
extra={"columnList": full_fields},
extra_query={"filterMode": "PROFESSIONAL"},
)