chart_config_builder.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. # Copyright 2021-2024 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import re
  12. import typing as t
  13. from enum import Enum
  14. from .._renderers.utils import _get_columns_dict
  15. from .._warnings import _warn
  16. from ..types import PropertyType
  17. from ..utils import _MapDict
  18. if t.TYPE_CHECKING:
  19. from ..gui import Gui
  20. class _Chart_iprops(Enum):
  21. x = 0
  22. y = 1
  23. z = 2
  24. label = 3
  25. text = 4
  26. mode = 5
  27. type = 6
  28. color = 7
  29. xaxis = 8
  30. yaxis = 9
  31. selected_color = 10
  32. marker = 11
  33. selected_marker = 12
  34. orientation = 13
  35. _name = 14
  36. line = 15
  37. text_anchor = 16
  38. options = 17
  39. lon = 18
  40. lat = 19
  41. base = 20
  42. r = 21
  43. theta = 22
  44. close = 23
  45. open = 24
  46. high = 25
  47. low = 26
  48. locations = 27
  49. values = 28
  50. labels = 29
  51. decimator = 30
  52. measure = 31
  53. parents = 32
  54. __CHART_AXIS: t.Dict[str, t.Tuple[_Chart_iprops, ...]] = {
  55. "bar": (_Chart_iprops.x, _Chart_iprops.y, _Chart_iprops.base),
  56. "candlestick": (
  57. _Chart_iprops.x,
  58. _Chart_iprops.close,
  59. _Chart_iprops.open,
  60. _Chart_iprops.high,
  61. _Chart_iprops.low,
  62. ),
  63. "choropleth": (_Chart_iprops.locations, _Chart_iprops.z),
  64. "choroplethmap": (_Chart_iprops.locations, _Chart_iprops.z),
  65. "choroplethmapbox": (_Chart_iprops.locations, _Chart_iprops.z),
  66. "densitymap": (_Chart_iprops.lon, _Chart_iprops.lat, _Chart_iprops.z),
  67. "densitymapbox": (_Chart_iprops.lon, _Chart_iprops.lat, _Chart_iprops.z),
  68. "funnelarea": (_Chart_iprops.values,),
  69. "pie": (_Chart_iprops.values, _Chart_iprops.labels),
  70. "scattergeo": (_Chart_iprops.lon, _Chart_iprops.lat),
  71. "scattermap": (_Chart_iprops.lon, _Chart_iprops.lat),
  72. "scattermapbox": (_Chart_iprops.lon, _Chart_iprops.lat),
  73. "scatterpolar": (_Chart_iprops.r, _Chart_iprops.theta),
  74. "scatterpolargl": (_Chart_iprops.r, _Chart_iprops.theta),
  75. "treemap": (_Chart_iprops.labels, _Chart_iprops.parents, _Chart_iprops.values),
  76. "waterfall": (_Chart_iprops.x, _Chart_iprops.y, _Chart_iprops.measure),
  77. }
  78. __CHART_DEFAULT_AXIS: t.Tuple[_Chart_iprops, ...] = (_Chart_iprops.x, _Chart_iprops.y, _Chart_iprops.z)
  79. __CHART_MARKER_TO_COLS: t.Tuple[str, ...] = ("color", "size", "symbol", "opacity", "colors")
  80. __CHART_NO_INDEX: t.Tuple[str, ...] = ("pie", "histogram", "heatmap", "funnelarea")
  81. _CHART_NAMES: t.Tuple[str, ...] = tuple(e.name[1:] if e.name[0] == "_" else e.name for e in _Chart_iprops)
  82. def __check_dict(values: t.List[t.Any], properties: t.Iterable[_Chart_iprops]) -> None:
  83. for prop in properties:
  84. if values[prop.value] is not None and not isinstance(values[prop.value], (dict, _MapDict)):
  85. _warn(f"Property {prop.name} of chart control should be a dict.")
  86. values[prop.value] = None
  87. def __get_multiple_indexed_attributes(
  88. attributes: t.Dict[str, t.Any], names: t.Iterable[str], index: t.Optional[int] = None
  89. ) -> t.List[t.Optional[str]]:
  90. names = names if index is None else [f"{n}[{index}]" for n in names] # type: ignore
  91. return [attributes.get(name) for name in names]
  92. __RE_INDEXED_DATA = re.compile(r"^(\d+)\/(.*)")
  93. def __get_col_from_indexed(col_name: str, idx: int) -> t.Optional[str]:
  94. if re_res := __RE_INDEXED_DATA.search(col_name):
  95. return col_name if str(idx) == re_res.group(1) else None
  96. return col_name
  97. def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t.Dict[str, str]): # noqa: C901
  98. if "data" not in attributes and "figure" in attributes:
  99. return {"traces": []}
  100. default_type = attributes.get("_default_type", "scatter")
  101. default_mode = attributes.get("_default_mode", "lines+markers")
  102. trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES)
  103. if not trace[_Chart_iprops.mode.value]:
  104. trace[_Chart_iprops.mode.value] = default_mode
  105. # type
  106. if not trace[_Chart_iprops.type.value]:
  107. trace[_Chart_iprops.type.value] = default_type
  108. if not trace[_Chart_iprops.xaxis.value]:
  109. trace[_Chart_iprops.xaxis.value] = "x"
  110. if not trace[_Chart_iprops.yaxis.value]:
  111. trace[_Chart_iprops.yaxis.value] = "y"
  112. # Indexed properties: Check for arrays
  113. for prop in _Chart_iprops:
  114. values = trace[prop.value]
  115. if isinstance(values, (list, tuple)) and len(values):
  116. prop_name = prop.name[1:] if prop.name[0] == "_" else prop.name
  117. for idx, val in enumerate(values):
  118. if idx == 0:
  119. trace[prop.value] = val
  120. if val is not None:
  121. indexed_prop = f"{prop_name}[{idx + 1}]"
  122. if attributes.get(indexed_prop) is None:
  123. attributes[indexed_prop] = val
  124. # marker selected_marker options
  125. __check_dict(trace, (_Chart_iprops.marker, _Chart_iprops.selected_marker, _Chart_iprops.options))
  126. axis = []
  127. traces: t.List[t.List[t.Optional[str]]] = []
  128. idx = 1
  129. indexed_trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES, idx)
  130. if len([x for x in indexed_trace if x]):
  131. while len([x for x in indexed_trace if x]):
  132. axis.append(
  133. __CHART_AXIS.get(
  134. indexed_trace[_Chart_iprops.type.value] or trace[_Chart_iprops.type.value] or "",
  135. __CHART_DEFAULT_AXIS,
  136. )
  137. )
  138. # marker selected_marker options
  139. __check_dict(indexed_trace, (_Chart_iprops.marker, _Chart_iprops.selected_marker, _Chart_iprops.options))
  140. if trace[_Chart_iprops.decimator.value] and not indexed_trace[_Chart_iprops.decimator.value]:
  141. # copy the decimator only once
  142. indexed_trace[_Chart_iprops.decimator.value] = trace[_Chart_iprops.decimator.value]
  143. trace[_Chart_iprops.decimator.value] = None
  144. traces.append([x or trace[i] for i, x in enumerate(indexed_trace)])
  145. idx += 1
  146. indexed_trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES, idx)
  147. else:
  148. traces.append(trace)
  149. # axis names
  150. axis.append(__CHART_AXIS.get(trace[_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS))
  151. # list of data columns name indexes with label text
  152. dt_idx = tuple(e.value for e in (axis[0] + (_Chart_iprops.label, _Chart_iprops.text)))
  153. # configure columns
  154. columns: t.Set[str] = set()
  155. for j, trace in enumerate(traces):
  156. dt_idx = tuple(
  157. e.value for e in (axis[j] if j < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text)
  158. )
  159. columns.update([trace[i] or "" for i in dt_idx if trace[i]])
  160. # add optional column if any
  161. markers = [
  162. t[_Chart_iprops.marker.value]
  163. or ({"color": t[_Chart_iprops.color.value]} if t[_Chart_iprops.color.value] else None)
  164. for t in traces
  165. ]
  166. opt_cols = set()
  167. for m in markers:
  168. if isinstance(m, (dict, _MapDict)):
  169. for prop1 in __CHART_MARKER_TO_COLS:
  170. val = m.get(prop1)
  171. if isinstance(val, str) and val not in columns:
  172. opt_cols.add(val)
  173. # Validate the column names
  174. col_dict = _get_columns_dict(attributes.get("data"), list(columns), col_types, opt_columns=opt_cols)
  175. # Manage Decimator
  176. decimators: t.List[t.Optional[str]] = []
  177. for tr in traces:
  178. if tr[_Chart_iprops.decimator.value]:
  179. cls = gui._get_user_instance(
  180. class_name=str(tr[_Chart_iprops.decimator.value]), class_type=PropertyType.decimator.value
  181. )
  182. if isinstance(cls, PropertyType.decimator.value):
  183. decimators.append(str(tr[_Chart_iprops.decimator.value]))
  184. continue
  185. decimators.append(None)
  186. # set default columns if not defined
  187. icols = [
  188. [c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in t.cast(dict, col_dict).keys()] if c2]
  189. for i in range(len(traces))
  190. ]
  191. for i, tr in enumerate(traces):
  192. if i < len(axis):
  193. used_cols = {tr[ax.value] for ax in axis[i] if tr[ax.value]}
  194. unused_cols = [c for c in icols[i] if c not in used_cols]
  195. if unused_cols and not any(tr[ax.value] for ax in axis[i]):
  196. traces[i] = [
  197. v or (unused_cols.pop(0) if unused_cols and _Chart_iprops(j) in axis[i] else v)
  198. for j, v in enumerate(tr)
  199. ]
  200. if col_dict is not None:
  201. reverse_cols = {str(cd.get("dfid")): c for c, cd in col_dict.items()}
  202. # List used axis
  203. used_axis = [[e for e in (axis[j] if j < len(axis) else axis[0]) if tr[e.value]] for j, tr in enumerate(traces)]
  204. ret_dict = {
  205. "columns": col_dict,
  206. "labels": [
  207. reverse_cols.get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or ""))
  208. for tr in traces
  209. ],
  210. "texts": [
  211. reverse_cols.get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None))
  212. for tr in traces
  213. ],
  214. "modes": [tr[_Chart_iprops.mode.value] for tr in traces],
  215. "types": [tr[_Chart_iprops.type.value] for tr in traces],
  216. "xaxis": [tr[_Chart_iprops.xaxis.value] for tr in traces],
  217. "yaxis": [tr[_Chart_iprops.yaxis.value] for tr in traces],
  218. "markers": markers,
  219. "selectedMarkers": [
  220. tr[_Chart_iprops.selected_marker.value]
  221. or (
  222. {"color": tr[_Chart_iprops.selected_color.value]}
  223. if tr[_Chart_iprops.selected_color.value]
  224. else None
  225. )
  226. for tr in traces
  227. ],
  228. "traces": [
  229. [reverse_cols.get(c or "", c) for c in [tr[e.value] for e in used_axis[j]]]
  230. for j, tr in enumerate(traces)
  231. ],
  232. "orientations": [tr[_Chart_iprops.orientation.value] for tr in traces],
  233. "names": [tr[_Chart_iprops._name.value] for tr in traces],
  234. "lines": [
  235. (
  236. tr[_Chart_iprops.line.value]
  237. if isinstance(tr[_Chart_iprops.line.value], (dict, _MapDict))
  238. else {"dash": tr[_Chart_iprops.line.value]}
  239. if tr[_Chart_iprops.line.value]
  240. else None
  241. )
  242. for tr in traces
  243. ],
  244. "textAnchors": [tr[_Chart_iprops.text_anchor.value] for tr in traces],
  245. "options": [tr[_Chart_iprops.options.value] for tr in traces],
  246. "axisNames": [[e.name for e in ax] for ax in used_axis],
  247. "addIndex": [tr[_Chart_iprops.type.value] not in __CHART_NO_INDEX for tr in traces],
  248. }
  249. if len([d for d in decimators if d]):
  250. ret_dict.update(decimators=decimators)
  251. return ret_dict
  252. return {}