Skip to content

Commit

Permalink
Chart: Support indexed data
Browse files Browse the repository at this point in the history
resolves #2338
  • Loading branch information
Fred Lefévère-Laoide authored and Fred Lefévère-Laoide committed Jan 10, 2025
1 parent b85c290 commit 6d31bee
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 173 deletions.
8 changes: 4 additions & 4 deletions frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const chartValue = {
},
};
const chartConfig = JSON.stringify({
columns: { Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } },
columns: [{ Day_str: { dfid: "Day" }, "Daily hospital occupancy": { dfid: "Daily hospital occupancy" } }],
traces: [["Day_str", "Daily hospital occupancy"]],
xaxis: ["x"],
yaxis: ["y"],
Expand All @@ -86,7 +86,7 @@ const mapValue = {
},
};
const mapConfig = JSON.stringify({
columns: { Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } },
columns: [{ Lat: { dfid: "Lat" }, Lon: { dfid: "Lon" } }],
traces: [["Lat", "Lon"]],
xaxis: ["x"],
yaxis: ["y"],
Expand Down Expand Up @@ -173,7 +173,7 @@ describe("Chart Component", () => {
payload: { id: "chart", names: ["varName"], refresh: false },
type: "REQUEST_UPDATE",
});
expect(dispatch).toHaveBeenCalledWith({
await waitFor(() => expect(dispatch).toHaveBeenCalledWith({
name: "data_var",
payload: {
alldata: true,
Expand All @@ -183,7 +183,7 @@ describe("Chart Component", () => {
id: "chart",
},
type: "REQUEST_DATA_UPDATE",
});
}));
});
it("dispatch a well formed message on selection", async () => {
const dispatch = jest.fn();
Expand Down
373 changes: 229 additions & 144 deletions frontend/taipy-gui/src/components/Taipy/Chart.tsx

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions taipy/gui/_renderers/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,31 @@ def _get_chart_config(self, default_type: str, default_mode: str):
self.__attributes["_default_mode"] = default_mode
rebuild_fn_hash = self.__build_rebuild_fn(
self.__gui._get_call_method_name("_chart_conf"),
_CHART_NAMES + ("_default_type", "_default_mode", "data"),
_CHART_NAMES + ("_default_type", "_default_mode"),
)
if rebuild_fn_hash:
self.__set_react_attribute("config", rebuild_fn_hash)

# read column definitions
data = self.__attributes.get("data")
data_hash = self.__hashes.get("data", "")
col_types = self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))
col_types = [self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))]

if data_hash:
data_updates: t.List[str] = []
data_idx = 1
name_idx = f"data[{data_idx}]"
while add_data_hash := self.__hashes.get(name_idx):
typed_hash = self.__get_typed_hash_name(add_data_hash, _TaipyData)
data_updates.append(typed_hash)
self.__set_react_attribute(f"data{data_idx}",_get_client_var_name(typed_hash))
add_data = self.__attributes.get(name_idx)
data_idx += 1
name_idx = f"data[{data_idx}]"
col_types.append(
self.__gui._get_accessor().get_col_types(add_data_hash, _TaipyData(add_data, add_data_hash))
)
self.set_attribute("dataVarNames", ";".join(data_updates))

config = _build_chart_config(self.__gui, self.__attributes, col_types)

Expand Down
11 changes: 9 additions & 2 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1903,11 +1903,18 @@ def _chart_conf(
rebuild = rebuild_val if rebuild_val is not None else rebuild
if rebuild:
attributes, hashes = self.__get_attributes(attr_json, hash_json, kwargs)
data_hash = hashes.get("data", "")
idx = 0
data_hashes = []
while data_hash := hashes.get("data" if idx == 0 else f"data[{idx}]", ""):
data_hashes.append(data_hash)
idx += 1
config = _build_chart_config(
self,
attributes,
self._get_accessor().get_col_types(data_hash, _TaipyData(kwargs.get(data_hash), data_hash)),
[
self._get_accessor().get_col_types(data_hash, _TaipyData(kwargs.get(data_hash), data_hash))
for data_hash in data_hashes
],
)

return json.dumps(config, cls=_TaipyJsonEncoder)
Expand Down
65 changes: 45 additions & 20 deletions taipy/gui/utils/chart_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def __get_col_from_indexed(col_name: str, idx: int) -> t.Optional[str]:
return col_name


def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t.Dict[str, str]): # noqa: C901
def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types_list: t.List[t.Dict[str, str]]): # noqa: C901
if "data" not in attributes and "figure" in attributes:
return {"traces": []}
default_type = attributes.get("_default_type", "scatter")
Expand Down Expand Up @@ -167,32 +167,47 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t
# axis names
axis.append(__CHART_AXIS.get(trace[_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS))

idx = 1
while f"data[{idx}]" in attributes:
if idx >= len(traces):
traces.append(list(traces[0]))
axis.append(__CHART_AXIS.get(traces[0][_Chart_iprops.type.value] or "", __CHART_DEFAULT_AXIS))
idx += 1

# list of data columns name indexes with label text
dt_idx = tuple(e.value for e in (axis[0] + (_Chart_iprops.label, _Chart_iprops.text)))

# configure columns
columns: t.Set[str] = set()
for j, trace in enumerate(traces):
columns: t.List[t.Set[str]] = [set()] * len(traces)
for idx, trace in enumerate(traces):
dt_idx = tuple(
e.value for e in (axis[j] if j < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text)
e.value for e in (axis[idx] if idx < len(axis) else axis[0]) + (_Chart_iprops.label, _Chart_iprops.text)
)
columns.update([trace[i] or "" for i in dt_idx if trace[i]])
columns[idx].update([trace[i] or "" for i in dt_idx if trace[i]])
# add optional column if any
markers = [
t[_Chart_iprops.marker.value]
or ({"color": t[_Chart_iprops.color.value]} if t[_Chart_iprops.color.value] else None)
for t in traces
]
opt_cols = set()
for m in markers:
opt_cols: t.List[t.Set[str]] = [set()] * len(traces)
for idx, m in enumerate(markers):
if isinstance(m, (dict, _MapDict)):
for prop1 in __CHART_MARKER_TO_COLS:
val = m.get(prop1)
if isinstance(val, str) and val not in columns:
opt_cols.add(val)
if isinstance(val, str) and val not in columns[idx]:
opt_cols[idx].add(val)

# Validate the column names
col_dict = _get_columns_dict(attributes.get("data"), list(columns), col_types, opt_columns=opt_cols)
col_dicts = []
for idx, col_types in enumerate(col_types_list):
if add_col_dict := _get_columns_dict(
attributes.get("data" if idx == 0 else f"data[{idx}]"),
list(columns[idx] if idx < len(columns) else columns[0]),
col_types,
opt_columns=opt_cols[idx] if idx < len(opt_cols) else opt_cols[0],
):
col_dicts.append(add_col_dict)

# Manage Decimator
decimators: t.List[t.Optional[str]] = []
Expand All @@ -208,7 +223,14 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t

# set default columns if not defined
icols = [
[c2 for c2 in [__get_col_from_indexed(c1, i) for c1 in t.cast(dict, col_dict).keys()] if c2]
[
c2
for c2 in [
__get_col_from_indexed(c1, i)
for c1 in t.cast(dict, col_dicts[i] if i < len(col_dicts) else col_dicts[0]).keys()
]
if c2
]
for i in range(len(traces))
]

Expand All @@ -222,21 +244,24 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t
for j, v in enumerate(tr)
]

if col_dict is not None:
reverse_cols = {str(cd.get("dfid")): c for c, cd in col_dict.items()}
if col_dicts:
reverse_cols = [{str(cd.get("dfid")): c for c, cd in col_dict.items()} for col_dict in col_dicts]
for idx in range(len(traces)):
if idx < len(reverse_cols):
reverse_cols.append(reverse_cols[0])

# List used axis
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)]

ret_dict = {
"columns": col_dict,
"columns": col_dicts,
"labels": [
reverse_cols.get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or ""))
for tr in traces
reverse_cols[idx].get(tr[_Chart_iprops.label.value] or "", (tr[_Chart_iprops.label.value] or ""))
for idx, tr in enumerate(traces)
],
"texts": [
reverse_cols.get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None))
for tr in traces
reverse_cols[idx].get(tr[_Chart_iprops.text.value] or "", (tr[_Chart_iprops.text.value] or None))
for idx, tr in enumerate(traces)
],
"modes": [tr[_Chart_iprops.mode.value] for tr in traces],
"types": [tr[_Chart_iprops.type.value] for tr in traces],
Expand All @@ -253,8 +278,8 @@ def _build_chart_config(gui: "Gui", attributes: t.Dict[str, t.Any], col_types: t
for tr in traces
],
"traces": [
[reverse_cols.get(c or "", c) for c in [tr[e.value] for e in used_axis[j]]]
for j, tr in enumerate(traces)
[reverse_cols[idx].get(c or "", c) for c in [tr[e.value] for e in used_axis[idx]]]
for idx, tr in enumerate(traces)
],
"orientations": [tr[_Chart_iprops.orientation.value] for tr in traces],
"names": [tr[_Chart_iprops._name.value] for tr in traces],
Expand Down
2 changes: 1 addition & 1 deletion taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@
"name": "data",
"default_property": true,
"required": true,
"type": "dynamic(Any)",
"type": "indexed(dynamic(Any))",
"doc": "The data object bound to this chart control.<br/>See the section on the <a href=\"#the-data-property\"><i>data</i> property</a> below for more details."
},
{
Expand Down
18 changes: 18 additions & 0 deletions tests/gui/builder/control/test_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,21 @@ def test_chart_indexed_properties_with_arrays_builder(gui: Gui, helpers):
"&quot;lines&quot;: [null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;, null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;]", # noqa: E501
]
helpers.test_control_builder(gui, page, expected_list)

def test_chart_multi_data(gui: Gui, helpers, csvdata):
with tgb.Page(frame=None) as page:
tgb.chart( # type: ignore[attr-defined]
data="{csvdata}",
x="Day",
y="Daily hospital occupancy",
data__1="{csvdata}",
)
expected_list = [
"<Chart",
'updateVarName="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
'dataVarNames="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
"data={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
"data1={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
]
gui._set_frame(inspect.currentframe())
helpers.test_control_builder(gui, page, expected_list)
12 changes: 12 additions & 0 deletions tests/gui/control/test_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,15 @@ def test_chart_indexed_properties_with_arrays(gui: Gui, helpers):
"&quot;lines&quot;: [null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;, null, &#x7B;&quot;dash&quot;: &quot;dashdot&quot;&#x7D;, &#x7B;&quot;dash&quot;: &quot;dash&quot;&#x7D;]", # noqa: E501
]
helpers.test_control_md(gui, md, expected_list)

def test_chart_multi_data(gui: Gui, helpers, csvdata):
md_string = "<|{csvdata}|chart|x=Day|y=Daily hospital occupancy|data[1]={csvdata}|>"
expected_list = [
"<Chart",
'updateVarName="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
'dataVarNames="_TpD_tpec_TpExPr_csvdata_TPMDL_0"',
"data={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
"data1={_TpD_tpec_TpExPr_csvdata_TPMDL_0}",
]
gui._set_frame(inspect.currentframe())
helpers.test_control_md(gui, md_string, expected_list)

0 comments on commit 6d31bee

Please sign in to comment.