chart_config_builder.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. "densitymapbox": (_Chart_iprops.lon, _Chart_iprops.lat, _Chart_iprops.z),
  65. "funnelarea": (_Chart_iprops.values,),
  66. "pie": (_Chart_iprops.values, _Chart_iprops.labels),
  67. "scattergeo": (_Chart_iprops.lon, _Chart_iprops.lat),
  68. "scattermapbox": (_Chart_iprops.lon, _Chart_iprops.lat),
  69. "scatterpolar": (_Chart_iprops.r, _Chart_iprops.theta),
  70. "scatterpolargl": (_Chart_iprops.r, _Chart_iprops.theta),
  71. "treemap": (_Chart_iprops.labels, _Chart_iprops.parents, _Chart_iprops.values),
  72. "waterfall": (_Chart_iprops.x, _Chart_iprops.y, _Chart_iprops.measure),
  73. }
  74. __CHART_DEFAULT_AXIS: t.Tuple[_Chart_iprops, ...] = (_Chart_iprops.x, _Chart_iprops.y, _Chart_iprops.z)
  75. __CHART_MARKER_TO_COLS: t.Tuple[str, ...] = ("color", "size", "symbol", "opacity", "colors")
  76. __CHART_NO_INDEX: t.Tuple[str, ...] = ("pie", "histogram", "heatmap", "funnelarea")
  77. _CHART_NAMES: t.Tuple[str, ...] = tuple(e.name[1:] if e.name[0] == "_" else e.name for e in _Chart_iprops)
  78. def __check_dict(values: t.List[t.Any], properties: t.Iterable[_Chart_iprops]) -> None:
  79. for prop in properties:
  80. if values[prop.value] is not None and not isinstance(values[prop.value], (dict, _MapDict)):
  81. _warn(f"Property {prop.name} of chart control should be a dict.")
  82. values[prop.value] = None
  83. def __get_multiple_indexed_attributes(
  84. attributes: t.Dict[str, t.Any], names: t.Iterable[str], index: t.Optional[int] = None
  85. ) -> t.List[t.Optional[str]]:
  86. names = names if index is None else [f"{n}[{index}]" for n in names] # type: ignore
  87. return [attributes.get(name) for name in names]
  88. __RE_INDEXED_DATA = re.compile(r"^(\d+)\/(.*)")
  89. def __get_col_from_indexed(col_name: str, idx: int) -> t.Optional[str]:
  90. if re_res := __RE_INDEXED_DATA.search(col_name):
  91. return col_name if str(idx) == re_res.group(1) else None
  92. return col_name
  93. def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t.Dict[str, str]): # noqa: C901
  94. default_type = attributes.get("_default_type", "scatter")
  95. default_mode = attributes.get("_default_mode", "lines+markers")
  96. trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES)
  97. if not trace[_Chart_iprops.mode.value]:
  98. trace[_Chart_iprops.mode.value] = default_mode
  99. # type
  100. if not trace[_Chart_iprops.type.value]:
  101. trace[_Chart_iprops.type.value] = default_type
  102. if not trace[_Chart_iprops.xaxis.value]:
  103. trace[_Chart_iprops.xaxis.value] = "x"
  104. if not trace[_Chart_iprops.yaxis.value]:
  105. trace[_Chart_iprops.yaxis.value] = "y"
  106. # Indexed properties: Check for arrays
  107. for prop in _Chart_iprops:
  108. values = trace[prop.value]
  109. if isinstance(values, (list, tuple)) and len(values):
  110. prop_name = prop.name[1:] if prop.name[0] == "_" else prop.name
  111. for idx, val in enumerate(values):
  112. if idx == 0:
  113. trace[prop.value] = val
  114. if val is not None:
  115. indexed_prop = f"{prop_name}[{idx + 1}]"
  116. if attributes.get(indexed_prop) is None:
  117. attributes[indexed_prop] = val
  118. # marker selected_marker options
  119. __check_dict(trace, (_Chart_iprops.marker, _Chart_iprops.selected_marker, _Chart_iprops.options))
  120. axis = []
  121. traces: t.List[t.List[t.Optional[str]]] = []
  122. idx = 1
  123. indexed_trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES, idx)
  124. if len([x for x in indexed_trace if x]):
  125. while len([x for x in indexed_trace if x]):
  126. axis.append(
  127. __CHART_AXIS.get(
  128. indexed_trace[_Chart_iprops.type.value] or trace[_Chart_iprops.type.value] or "",
  129. __CHART_DEFAULT_AXIS,
  130. )
  131. )
  132. # marker selected_marker options
  133. __check_dict(indexed_trace, (_Chart_iprops.marker, _Chart_iprops.selected_marker, _Chart_iprops.options))
  134. if trace[_Chart_iprops.decimator.value] and not indexed_trace[_Chart_iprops.decimator.value]:
  135. # copy the decimator only once
  136. indexed_trace[_Chart_iprops.decimator.value] = trace[_Chart_iprops.decimator.value]
  137. trace[_Chart_iprops.decimator.value] = None
  138. traces.append([x or trace[i] for i, x in enumerate(indexed_trace)])
  139. idx += 1
  140. indexed_trace = __get_multiple_indexed_attributes(attributes, _CHART_NAMES, idx)
  141. else:
  142. traces.append(trace)
  143. # axis names
  144. axis.append(__CHART_AXIS.get(trace[_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS))
  145. # list of data columns name indexes with label text
  146. dt_idx = tuple(e.value for e in (axis[0] + (_Chart_iprops.label, _Chart_iprops.text)))
  147. # configure columns
  148. columns: t.Set[str] = set()
  149. for j, trace in enumerate(traces):
  150. dt_idx = tuple(
  151. e.value for e in (axis[j] if j < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text)
  152. )
  153. columns.update([trace[i] or "" for i in dt_idx if trace[i]])
  154. # add optional column if any
  155. markers = [
  156. t[_Chart_iprops.marker.value]
  157. or ({"color": t[_Chart_iprops.color.value]} if t[_Chart_iprops.color.value] else None)
  158. for t in traces
  159. ]
  160. opt_cols = set()
  161. for m in markers:
  162. if isinstance(m, (dict, _MapDict)):
  163. for prop1 in __CHART_MARKER_TO_COLS:
  164. val = m.get(prop1)
  165. if isinstance(val, str) and val not in columns:
  166. opt_cols.add(val)
  167. # Validate the column names
  168. col_dict = _get_columns_dict(attributes.get("data"), list(columns), col_types, opt_columns=opt_cols)
  169. # Manage Decimator
  170. decimators: t.List[t.Optional[str]] = []
  171. for tr in traces:
  172. if tr[_Chart_iprops.decimator.value]:
  173. cls = gui._get_user_instance(
  174. class_name=str(tr[_Chart_iprops.decimator.value]), class_type=PropertyType.decimator.value
  175. )
  176. if isinstance(cls, PropertyType.decimator.value):
  177. decimators.append(str(tr[_Chart_iprops.decimator.value]))
  178. continue
  179. decimators.append(None)
  180. # set default columns if not defined
  181. icols = [[c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in col_dict.keys()] if c2] for i in range(len(traces))]
  182. for i, tr in enumerate(traces):
  183. if i < len(axis):
  184. used_cols = {tr[ax.value] for ax in axis[i] if tr[ax.value]}
  185. unused_cols = [c for c in icols[i] if c not in used_cols]
  186. if unused_cols and not any(tr[ax.value] for ax in axis[i]):
  187. traces[i] = [
  188. v or (unused_cols.pop(0) if unused_cols and _Chart_iprops(j) in axis[i] else v)
  189. for j, v in enumerate(tr)
  190. ]
  191. if col_dict is not None:
  192. reverse_cols = {str(cd.get("dfid")): c for c, cd in col_dict.items()}
  193. # List used axis
  194. 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)]
  195. ret_dict = {
  196. "columns": col_dict,
  197. "labels": [
  198. reverse_cols.get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or ""))
  199. for tr in traces
  200. ],
  201. "texts": [
  202. reverse_cols.get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None))
  203. for tr in traces
  204. ],
  205. "modes": [tr[_Chart_iprops.mode.value] for tr in traces],
  206. "types": [tr[_Chart_iprops.type.value] for tr in traces],
  207. "xaxis": [tr[_Chart_iprops.xaxis.value] for tr in traces],
  208. "yaxis": [tr[_Chart_iprops.yaxis.value] for tr in traces],
  209. "markers": markers,
  210. "selectedMarkers": [
  211. tr[_Chart_iprops.selected_marker.value]
  212. or (
  213. {"color": tr[_Chart_iprops.selected_color.value]}
  214. if tr[_Chart_iprops.selected_color.value]
  215. else None
  216. )
  217. for tr in traces
  218. ],
  219. "traces": [
  220. [reverse_cols.get(c or "", c) for c in [tr[e.value] for e in used_axis[j]]]
  221. for j, tr in enumerate(traces)
  222. ],
  223. "orientations": [tr[_Chart_iprops.orientation.value] for tr in traces],
  224. "names": [tr[_Chart_iprops._name.value] for tr in traces],
  225. "lines": [
  226. (
  227. tr[_Chart_iprops.line.value]
  228. if isinstance(tr[_Chart_iprops.line.value], (dict, _MapDict))
  229. else {"dash": tr[_Chart_iprops.line.value]}
  230. if tr[_Chart_iprops.line.value]
  231. else None
  232. )
  233. for tr in traces
  234. ],
  235. "textAnchors": [tr[_Chart_iprops.text_anchor.value] for tr in traces],
  236. "options": [tr[_Chart_iprops.options.value] for tr in traces],
  237. "axisNames": [[e.name for e in ax] for ax in used_axis],
  238. "addIndex": [tr[_Chart_iprops.type.value] not in __CHART_NO_INDEX for tr in traces],
  239. }
  240. if len([d for d in decimators if d]):
  241. ret_dict.update(decimators=decimators)
  242. return ret_dict
  243. return {}