namnguyen 10 месяцев назад
Родитель
Сommit
cca47ed85e

+ 346 - 51
frontend/taipy-gui/src/components/Taipy/Chart.spec.tsx

@@ -11,14 +11,23 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React from "react";
-import { render, waitFor } from "@testing-library/react";
+import React, {useCallback} from "react";
+import {render, renderHook, waitFor} from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
-import Chart, { TraceValueType } from "./Chart";
-import { TaipyContext } from "../../context/taipyContext";
-import { TaipyState, INITIAL_STATE } from "../../context/taipyReducers";
+import Chart, {
+    getAxis,
+    getColNameFromIndexed,
+    getValue,
+    getValueFromCol,
+    TaipyPlotlyButtons,
+    TraceValueType
+} from "./Chart";
+import {TaipyContext} from "../../context/taipyContext";
+import {INITIAL_STATE, TaipyState} from "../../context/taipyReducers";
+import {ColumnDesc} from "./tableUtils";
+import {ModeBarButtonAny} from "plotly.js";
 
 const chartValue = {
     default: {
@@ -33,7 +42,7 @@ const chartValue = {
             "2020-04-07T00:00:00.000000Z",
             "2020-04-08T00:00:00.000000Z",
             "2020-04-09T00:00:00.000000Z",
-            "2020-04-10T00:00:00.000000Z",
+            "2020-04-10T00:00:00.000000Z"
         ],
         Entity: [
             "Austria",
@@ -45,13 +54,13 @@ const chartValue = {
             "Austria",
             "Austria",
             "Austria",
-            "Austria",
+            "Austria"
         ],
-        "Daily hospital occupancy": [856, 823, 829, 826, 712, 824, 857, 829, 820, 771],
-    },
+        "Daily hospital occupancy": [856, 823, 829, 826, 712, 824, 857, 829, 820, 771]
+    }
 };
 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"],
@@ -64,20 +73,20 @@ const mapValue = {
     default: {
         Lat: [
             48.4113, 18.0057, 48.6163, 48.5379, 48.5843, 48.612, 48.6286, 48.6068, 48.4489, 48.6548, 18.5721, 48.3734,
-            17.6398, 48.5765, 48.4407, 48.2286,
+            17.6398, 48.5765, 48.4407, 48.2286
         ],
         Lon: [
             -112.8352, -65.804, -113.4784, -114.0702, -111.0188, -110.7939, -109.4629, -114.9123, -112.9705, -113.965,
-            -66.5401, -111.5245, -64.7246, -112.1932, -113.3159, -104.5863,
+            -66.5401, -111.5245, -64.7246, -112.1932, -113.3159, -104.5863
         ],
         Globvalue: [
             0.0875, 0.0892, 0.0908, 0.0933, 0.0942, 0.095, 0.095, 0.095, 0.0958, 0.0958, 0.0958, 0.0958, 0.0958, 0.0975,
-            0.0983, 0.0992,
-        ],
-    },
+            0.0983, 0.0992
+        ]
+    }
 };
 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"],
@@ -88,51 +97,87 @@ const mapConfig = JSON.stringify({
 
 const mapLayout = JSON.stringify({
     dragmode: "zoom",
-    mapbox: { style: "open-street-map", center: { lat: 38, lon: -90 }, zoom: 3 },
-    margin: { r: 0, t: 0, b: 0, l: 0 }
-})
+    mapbox: {style: "open-street-map", center: {lat: 38, lon: -90}, zoom: 3},
+    margin: {r: 0, t: 0, b: 0, l: 0}
+});
+
+interface Props {
+    figure?: boolean;
+}
+
+interface Clickable {
+    click: (gd: HTMLElement, evt: Event) => void;
+}
+
+type DataKey = string;
+type Data = Record<DataKey, {tp_index?: number[]}>;
+
+const useGetRealIndex = (data: Data, dataKey: DataKey, props: Props) => {
+    return useCallback(
+        (index?: number) =>
+            typeof index === "number"
+                ? props.figure
+                    ? index
+                    : data[dataKey] && data[dataKey].tp_index
+                      ? data[dataKey]!.tp_index![index]
+                      : index
+                : 0,
+        [data, dataKey, props.figure]
+    );
+};
 
 describe("Chart Component", () => {
     it("renders", async () => {
-        const { getByTestId } = render(<Chart data={chartValue} defaultConfig={chartConfig} testId="test" />);
+        const {getByTestId} = render(<Chart data={chartValue} defaultConfig={chartConfig} testId="test" />);
         const elt = getByTestId("test");
         expect(elt.tagName).toBe("DIV");
     });
     it("displays the right info for class", async () => {
-        const { getByTestId } = render(
+        const {getByTestId} = render(
             <Chart data={chartValue} defaultConfig={chartConfig} testId="test" className="taipy-chart" />
         );
         const elt = getByTestId("test");
         expect(elt).toHaveClass("taipy-chart");
     });
     it("is disabled", async () => {
-        const { getByTestId } = render(<Chart data={chartValue} defaultConfig={chartConfig} testId="test" active={false} />);
+        const {getByTestId} = render(
+            <Chart data={chartValue} defaultConfig={chartConfig} testId="test" active={false} />
+        );
         const elt = getByTestId("test");
         expect(elt.querySelector(".modebar")).toBeNull();
     });
     it("is enabled by default", async () => {
-        const { getByTestId } = render(<Chart data={undefined} defaultConfig={chartConfig} testId="test" />);
+        const {getByTestId} = render(<Chart data={undefined} defaultConfig={chartConfig} testId="test" />);
         const elt = getByTestId("test");
         await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
     });
     it("is enabled by active", async () => {
-        const { getByTestId } = render(<Chart data={undefined} defaultConfig={chartConfig} testId="test" active={true} />);
+        const {getByTestId} = render(
+            <Chart data={undefined} defaultConfig={chartConfig} testId="test" active={true} />
+        );
         const elt = getByTestId("test");
         await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
     });
     it("dispatch 2 well formed messages at first render", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const selProps = { selected0: JSON.stringify([2, 4, 6]) };
+        const selProps = {selected0: JSON.stringify([2, 4, 6])};
         render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
-                <Chart id="chart" data={undefined} updateVarName="data_var" defaultConfig={chartConfig} updateVars="varname=varname" {...selProps} />
+            <TaipyContext.Provider value={{state, dispatch}}>
+                <Chart
+                    id="chart"
+                    data={undefined}
+                    updateVarName="data_var"
+                    defaultConfig={chartConfig}
+                    updateVars="varname=varname"
+                    {...selProps}
+                />
             </TaipyContext.Provider>
         );
         expect(dispatch).toHaveBeenCalledWith({
             name: "",
-            payload: { id: "chart", names: ["varname"], refresh: false },
-            type: "REQUEST_UPDATE",
+            payload: {id: "chart", names: ["varname"], refresh: false},
+            type: "REQUEST_UPDATE"
         });
         expect(dispatch).toHaveBeenCalledWith({
             name: "data_var",
@@ -141,39 +186,39 @@ describe("Chart Component", () => {
                 pagekey: "Day-Daily hospital occupancy",
                 columns: ["Day", "Daily hospital occupancy"],
                 decimatorPayload: undefined,
-                id: "chart",
+                id: "chart"
             },
-            type: "REQUEST_DATA_UPDATE",
+            type: "REQUEST_DATA_UPDATE"
         });
     });
     it("dispatch a well formed message on selection", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;
-        const { getByTestId } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
+        const {getByTestId} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
                 <Chart data={undefined} updateVarName="data_var" defaultConfig={chartConfig} testId="test" />
             </TaipyContext.Provider>
         );
         const elt = getByTestId("test");
         await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
         const modebar = elt.querySelector(".modebar");
-        modebar && await userEvent.click(modebar);
+        modebar && (await userEvent.click(modebar));
         expect(dispatch).toHaveBeenCalledWith({
             name: "data_var",
             payload: {
                 alldata: true,
-                columns: ["Day","Daily hospital occupancy"],
+                columns: ["Day", "Daily hospital occupancy"],
                 decimatorPayload: undefined,
-                pagekey: "Day-Daily hospital occupancy",
+                pagekey: "Day-Daily hospital occupancy"
             },
-            type: "REQUEST_DATA_UPDATE",
+            type: "REQUEST_DATA_UPDATE"
         });
     });
     xit("dispatch a well formed message on relayout", async () => {
         const dispatch = jest.fn();
-        const state: TaipyState = { ...INITIAL_STATE, data: { table: undefined } };
-        const { getByLabelText, rerender } = render(
-            <TaipyContext.Provider value={{ state, dispatch }}>
+        const state: TaipyState = {...INITIAL_STATE, data: {table: undefined}};
+        const {getByLabelText, rerender} = render(
+            <TaipyContext.Provider value={{state, dispatch}}>
                 <Chart
                     id="table"
                     updateVarName="data_var"
@@ -183,9 +228,9 @@ describe("Chart Component", () => {
                 />
             </TaipyContext.Provider>
         );
-        const newState = { ...state, data: { ...state.data, table: chartValue } };
+        const newState = {...state, data: {...state.data, table: chartValue}};
         rerender(
-            <TaipyContext.Provider value={{ state: newState, dispatch }}>
+            <TaipyContext.Provider value={{state: newState, dispatch}}>
                 <Chart
                     id="table"
                     updateVarName="data_var"
@@ -206,13 +251,13 @@ describe("Chart Component", () => {
                 orderby: "",
                 pagekey: "100-200--asc",
                 sort: "asc",
-                start: 100,
+                start: 100
             },
-            type: "REQUEST_DATA_UPDATE",
+            type: "REQUEST_DATA_UPDATE"
         });
     });
     xit("displays the received data", async () => {
-        const { getAllByText, rerender } = render(
+        const {getAllByText, rerender} = render(
             <Chart data={undefined} defaultConfig={chartConfig} updateVars="varname=varname" />
         );
         rerender(<Chart data={chartValue} defaultConfig={chartConfig} updateVars="varname=varname" />);
@@ -220,11 +265,261 @@ describe("Chart Component", () => {
         expect(elts.length).toBeGreaterThan(1);
         expect(elts[0].tagName).toBe("TD");
     });
-    describe("Chart Component as Map", () => {
-        it("renders", async () => {
-            const { getByTestId } = render(<Chart data={mapValue} defaultConfig={mapConfig} layout={mapLayout} testId="test" />);
-            const elt = getByTestId("test");
-            await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
-        });
+
+    it("Chart renders correctly", () => {
+        const figure = [{data: [], layout: {title: "Mock Title"}}];
+        const {asFragment} = render(
+            <Chart
+                id="table"
+                updateVarName="data_var"
+                data={undefined}
+                defaultConfig={chartConfig}
+                updateVars="varname=varname"
+                figure={figure}
+            />
+        );
+        expect(asFragment()).toMatchSnapshot();
+    });
+
+    it("handles plotConfig prop correctly", () => {
+        const consoleInfoSpy = jest.spyOn(console, "info");
+        // Case 1: plotConfig is a valid JSON string
+        render(<Chart plotConfig='{"autosizable": true}' defaultConfig={chartConfig} />);
+        expect(consoleInfoSpy).not.toHaveBeenCalled();
+        // Case 2: plotConfig is not a valid JSON string
+        render(<Chart plotConfig="not a valid json" defaultConfig={chartConfig} />);
+        expect(consoleInfoSpy).toHaveBeenCalledWith(
+            "Error while parsing Chart.plot_config\nUnexpected token 'o', \"not a valid json\" is not valid JSON"
+        );
+        // Case 3: plotConfig is not an object
+        render(<Chart plotConfig='"not an object"' defaultConfig={chartConfig} />);
+        expect(consoleInfoSpy).toHaveBeenCalledWith("Error Chart.plot_config is not a dictionary");
+        consoleInfoSpy.mockRestore();
+    });
+});
+
+describe("Chart Component as Map", () => {
+    it("renders", async () => {
+        const {getByTestId} = render(
+            <Chart data={mapValue} defaultConfig={mapConfig} layout={mapLayout} testId="test" />
+        );
+        const elt = getByTestId("test");
+        await waitFor(() => expect(elt.querySelector(".modebar")).not.toBeNull());
+    });
+});
+
+describe("getColNameFromIndexed function", () => {
+    it("returns the column name when the input string matches the pattern", () => {
+        const colName = "1/myColumn";
+        const result = getColNameFromIndexed(colName);
+        expect(result).toBe("myColumn");
+    });
+    it("returns the input string when the input string does not match the pattern", () => {
+        const colName = "myColumn";
+        const result = getColNameFromIndexed(colName);
+        expect(result).toBe("myColumn");
+    });
+    it("returns the input string when the input string is empty", () => {
+        const colName = "";
+        const result = getColNameFromIndexed(colName);
+        expect(result).toBe("");
+    });
+});
+
+describe("getValue function", () => {
+    it("returns the value from column when the input string matches the pattern", () => {
+        const values: TraceValueType = {myColumn: [1, 2, 3]};
+        const arr: string[] = ["myColumn"];
+        const idx = 0;
+        const result = getValue(values, arr, idx);
+        expect(result).toEqual([1, 2, 3]);
+    });
+
+    it("returns undefined when returnUndefined is true and value is empty", () => {
+        const values: TraceValueType = {myColumn: []};
+        const arr: string[] = ["myColumn"];
+        const idx = 0;
+        const returnUndefined = true;
+        const result = getValue(values, arr, idx, returnUndefined);
+        expect(result).toBeUndefined();
+    });
+
+    it("returns empty array when returnUndefined is false and value is empty", () => {
+        const values: TraceValueType = {myColumn: []};
+        const arr: string[] = ["myColumn"];
+        const idx = 0;
+        const returnUndefined = false;
+        const result = getValue(values, arr, idx, returnUndefined);
+        expect(result).toEqual([]);
+    });
+});
+
+describe("getRealIndex function", () => {
+    it("should return 0 if index is not a number", () => {
+        const data = {};
+        const dataKey = "someKey";
+        const props = {figure: false};
+
+        const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
+        const getRealIndex = result.current;
+        expect(getRealIndex(undefined)).toBe(0);
+    });
+
+    it("should return index if figure is true", () => {
+        const data = {};
+        const dataKey = "someKey";
+        const props = {figure: true};
+
+        const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
+        const getRealIndex = result.current;
+        expect(getRealIndex(5)).toBe(5); // index is a number
+    });
+
+    it("should return index if figure is false and tp_index does not exist", () => {
+        const data = {
+            someKey: {}
+        };
+        const dataKey = "someKey";
+        const props = {figure: false};
+
+        const {result} = renderHook(() => useGetRealIndex(data, dataKey, props));
+        const getRealIndex = result.current;
+        expect(getRealIndex(2)).toBe(2); // index is a number
+    });
+});
+
+describe("getValueFromCol function", () => {
+    it("should return an empty array when values is undefined", () => {
+        const result = getValueFromCol(undefined, "test");
+        expect(result).toEqual([]);
+    });
+
+    it("should return an empty array when values[col] is undefined", () => {
+        const values = {test: [1, 2, 3]};
+        const result = getValueFromCol(values, "nonexistent");
+        expect(result).toEqual([]);
+    });
+});
+
+describe("getAxis function", () => {
+    it("should return undefined when traces length is less than idx", () => {
+        const traces = [["test"]];
+        const columns: Record<string, ColumnDesc> = {
+            test: {
+                dfid: "test",
+                type: "testType",
+                index: 0
+            }
+        };
+        const result = getAxis(traces, 2, columns, 0);
+        expect(result).toBeUndefined();
+    });
+
+    it("should return undefined when traces[idx] length is less than axis", () => {
+        const traces = [["test"]];
+        const columns: Record<string, ColumnDesc> = {
+            test: {
+                dfid: "test",
+                type: "testType",
+                index: 0
+            }
+        };
+        const result = getAxis(traces, 0, columns, 2);
+        expect(result).toBeUndefined();
+    });
+
+    it("should return undefined when traces[idx][axis] is undefined", () => {
+        const traces = [["test"]];
+        const columns: Record<string, ColumnDesc> = {
+            test: {
+                dfid: "test",
+                type: "testType",
+                index: 0
+            }
+        };
+        const result = getAxis(traces, 0, columns, 1);
+        expect(result).toBeUndefined();
+    });
+
+    it("should return undefined when columns[traces[idx][axis]] is undefined", () => {
+        const traces = [["test"]];
+        const columns: Record<string, ColumnDesc> = {
+            test: {
+                dfid: "test",
+                type: "testType",
+                index: 0
+            }
+        };
+        const result = getAxis(traces, 0, columns, 1); // changed axis from 0 to 1
+        expect(result).toBeUndefined();
+    });
+
+    it("should return dfid when all conditions are met", () => {
+        const traces = [["test"]];
+        const columns: Record<string, ColumnDesc> = {
+            test: {
+                dfid: "test",
+                type: "testType",
+                index: 0
+            }
+        };
+        const result = getAxis(traces, 0, columns, 0);
+        expect(result).toBe("test");
+    });
+});
+
+describe("click function", () => {
+    it("should return when div is not found", () => {
+        // Create a mock HTMLElement without 'div.svg-container'
+        const mockElement = document.createElement("div");
+        // Create a mock Event
+        const mockEvent = new Event("click");
+        // Call the click function with the mock HTMLElement and Event
+        (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
+        // Since there's no 'div.svg-container', the function should return without making any changes
+        // We can check this by verifying that no 'full-screen' class was added
+        expect(mockElement.classList.contains("full-screen")).toBe(false);
+    });
+    it("should set data-height when div.svg-container is found but data-height is not set", () => {
+        // Create a mock HTMLElement
+        const mockElement = document.createElement("div");
+
+        // Create a mock div with class 'svg-container' and append it to the mockElement
+        const mockDiv = document.createElement("div");
+        mockDiv.className = "svg-container";
+        mockElement.appendChild(mockDiv);
+
+        // Create a mock Event
+        const mockEvent = {
+            ...new Event("click"),
+            currentTarget: document.createElement("div")
+        };
+
+        // Call the click function with the mock HTMLElement and Event
+        (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
+
+        // Check that the 'data-height' attribute was set
+        expect(mockElement.getAttribute("data-height")).not.toBeNull();
+    });
+    it("should set data-title attribute", () => {
+        // Create a mock HTMLElement
+        const mockElement = document.createElement("div");
+
+        // Create a mock div with class 'svg-container' and append it to the mockElement
+        const mockDiv = document.createElement("div");
+        mockDiv.className = "svg-container";
+        mockElement.appendChild(mockDiv);
+
+        // Create a mock Event with a mock currentTarget
+        const mockEvent = {
+            ...new Event("click"),
+            currentTarget: mockElement
+        };
+
+        // Call the click function with the mock HTMLElement and Event
+        (TaipyPlotlyButtons[0] as ModeBarButtonAny & Clickable).click(mockElement, mockEvent);
+
+        // Check that the 'data-title' attribute was set
+        expect(mockElement.getAttribute("data-title")).toBe("Exit Full screen");
     });
 });

+ 116 - 115
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -21,7 +21,7 @@ import {
     PlotMarker,
     PlotRelayoutEvent,
     PlotSelectionEvent,
-    ScatterLine,
+    ScatterLine
 } from "plotly.js";
 import Skeleton from "@mui/material/Skeleton";
 import Box from "@mui/material/Box";
@@ -32,7 +32,7 @@ import {getArrayValue, getUpdateVar, TaipyActiveProps, TaipyChangeProps} from ".
 import {
     createRequestChartUpdateAction,
     createSendActionNameAction,
-    createSendUpdateAction,
+    createSendUpdateAction
 } from "../../context/taipyReducers";
 import {ColumnDesc} from "./tableUtils";
 import {
@@ -41,7 +41,7 @@ import {
     useDispatchRequestUpdateOnFirstRender,
     useDynamicJsonProperty,
     useDynamicProperty,
-    useModule,
+    useModule
 } from "../../utils/hooks";
 import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
 
@@ -95,7 +95,7 @@ const defaultStyle = {position: "relative", display: "inline-block"};
 
 const indexedData = /^(\d+)\/(.*)/;
 
-const getColNameFromIndexed = (colName: string): string => {
+export const getColNameFromIndexed = (colName: string): string => {
     if (colName) {
         const reRes = indexedData.exec(colName);
         if (reRes && reRes.length > 2) {
@@ -105,7 +105,7 @@ const getColNameFromIndexed = (colName: string): string => {
     return colName;
 };
 
-const getValue = <T, >(
+export const getValue = <T,>(
     values: TraceValueType | undefined,
     arr: T[],
     idx: number,
@@ -118,9 +118,10 @@ const getValue = <T, >(
     return undefined;
 };
 
-const getValueFromCol = (values: TraceValueType | undefined, col: string): (string | number)[] => {
+export const getValueFromCol = (values: TraceValueType | undefined, col: string): (string | number)[] => {
     if (values) {
         if (col) {
+            // TODO: Re-review the logic here
             if (Array.isArray(values)) {
                 const reRes = indexedData.exec(col);
                 if (reRes && reRes.length > 2) {
@@ -134,7 +135,7 @@ const getValueFromCol = (values: TraceValueType | undefined, col: string): (stri
     return [];
 };
 
-const getAxis = (traces: string[][], idx: number, columns: Record<string, ColumnDesc>, axis: number) => {
+export const getAxis = (traces: string[][], idx: number, columns: Record<string, ColumnDesc>, axis: number) => {
     if (traces.length > idx && traces[idx].length > axis && traces[idx][axis] && columns[traces[idx][axis]])
         return columns[traces[idx][axis]].dfid;
     return undefined;
@@ -150,21 +151,21 @@ const getDecimatorsPayload = (
 ) => {
     return decimators
         ? {
-            width: plotDiv?.clientWidth,
-            height: plotDiv?.clientHeight,
-            decimators: decimators.map((d, i) =>
-                d
-                    ? {
-                        decimator: d,
-                        xAxis: getAxis(traces, i, columns, 0),
-                        yAxis: getAxis(traces, i, columns, 1),
-                        zAxis: getAxis(traces, i, columns, 2),
-                        chartMode: modes[i],
-                    }
-                    : undefined
-            ),
-            relayoutData: relayoutData,
-        }
+              width: plotDiv?.clientWidth,
+              height: plotDiv?.clientHeight,
+              decimators: decimators.map((d, i) =>
+                  d
+                      ? {
+                            decimator: d,
+                            xAxis: getAxis(traces, i, columns, 0),
+                            yAxis: getAxis(traces, i, columns, 1),
+                            zAxis: getAxis(traces, i, columns, 2),
+                            chartMode: modes[i]
+                        }
+                      : undefined
+              ),
+              relayoutData: relayoutData
+          }
         : undefined;
 };
 
@@ -178,7 +179,7 @@ interface WithpointNumbers {
     pointNumbers: number[];
 }
 
-const getPlotIndex = (pt: PlotDatum) =>
+export const getPlotIndex = (pt: PlotDatum) =>
     pt.pointIndex === undefined
         ? pt.pointNumber === undefined
             ? (pt as unknown as WithpointNumbers).pointNumbers?.length
@@ -204,20 +205,20 @@ const defaultConfig = {
     textAnchors: [],
     options: [],
     axisNames: [],
-    addIndex: [],
+    addIndex: []
 } as ChartConfig;
 
 const emptyLayout = {} as Partial<Layout>;
 const emptyData = {} as Record<string, TraceValueType>;
 
-const TaipyPlotlyButtons: ModeBarButtonAny[] = [
+export const TaipyPlotlyButtons: ModeBarButtonAny[] = [
     {
         name: "Full screen",
         title: "Full screen",
         icon: {
             height: 24,
             width: 24,
-            path: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z",
+            path: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
         },
         click: function (gd: HTMLElement, evt: Event) {
             const div = gd.querySelector("div.svg-container") as HTMLDivElement;
@@ -234,8 +235,8 @@ const TaipyPlotlyButtons: ModeBarButtonAny[] = [
                 div.attributeStyleMap.set("height", height);
             }
             window.dispatchEvent(new Event("resize"));
-        },
-    },
+        }
+    }
 ];
 
 const updateArrays = (sel: number[][], val: number[], idx: number) => {
@@ -256,7 +257,7 @@ const Chart = (props: ChartProp) => {
         id,
         data = emptyData,
         onRangeChange,
-        propagate = true,
+        propagate = true
     } = props;
     const dispatch = useDispatch();
     const [selected, setSelected] = useState<number[][]>([]);
@@ -364,7 +365,7 @@ const Chart = (props: ChartProp) => {
                 ...(props.figure[0].layout as Partial<Layout>),
                 ...layout,
                 title: title || layout.title || (props.figure[0].layout as Partial<Layout>).title,
-                clickmode: "event+select",
+                clickmode: "event+select"
             } as Layout;
         }
         return {
@@ -376,16 +377,16 @@ const Chart = (props: ChartProp) => {
                     config.traces.length && config.traces[0].length && config.traces[0][0]
                         ? getColNameFromIndexed(config.columns[config.traces[0][0]]?.dfid)
                         : undefined,
-                ...layout.xaxis,
+                ...layout.xaxis
             },
             yaxis: {
                 title:
                     config.traces.length == 1 && config.traces[0].length > 1 && config.columns[config.traces[0][1]]
                         ? getColNameFromIndexed(config.columns[config.traces[0][1]]?.dfid)
                         : undefined,
-                ...layout.yaxis,
+                ...layout.yaxis
             },
-            clickmode: "event+select",
+            clickmode: "event+select"
         } as Layout;
     }, [
         theme.palette.mode,
@@ -396,7 +397,7 @@ const Chart = (props: ChartProp) => {
         props.template,
         props.template_Dark_,
         props.template_Light_,
-        props.figure,
+        props.figure
     ]);
 
     const style = useMemo(
@@ -418,83 +419,83 @@ const Chart = (props: ChartProp) => {
         const datum = data[dataKey];
         lastDataPl.current = datum
             ? config.traces.map((trace, idx) => {
-                const ret = {
-                    ...getArrayValue(config.options, idx, {}),
-                    type: config.types[idx],
-                    mode: config.modes[idx],
-                    name:
-                        getArrayValue(config.names, idx) ||
-                        (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined),
-                } as Record<string, unknown>;
-                ret.marker = {...getArrayValue(config.markers, idx, ret.marker || {})};
-                if (Object.keys(ret.marker as object).length) {
-                    MARKER_TO_COL.forEach((prop) => {
-                        const val = (ret.marker as Record<string, unknown>)[prop];
-                        if (typeof val === "string") {
-                            const arr = getValueFromCol(datum, val as string);
-                            if (arr.length) {
-                                (ret.marker as Record<string, unknown>)[prop] = arr;
-                            }
-                        }
-                    });
-                } else {
-                    delete ret.marker;
-                }
-                const xs = getValue(datum, trace, 0) || [];
-                const ys = getValue(datum, trace, 1) || [];
-                const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
-                const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
-                const baseY = addIndex ? xs : ys;
-                const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
-                if (baseX.length) {
-                    if (axisNames.length > 0) {
-                        ret[axisNames[0]] = baseX;
-                    } else {
-                        ret.x = baseX;
-                    }
-                }
-                if (baseY.length) {
-                    if (axisNames.length > 1) {
-                        ret[axisNames[1]] = baseY;
-                    } else {
-                        ret.y = baseY;
-                    }
-                }
-                const baseZ = getValue(datum, trace, 2, true);
-                if (baseZ) {
-                    if (axisNames.length > 2) {
-                        ret[axisNames[2]] = baseZ;
-                    } else {
-                        ret.z = baseZ;
-                    }
-                }
-                    // Hack for treemap charts: create a fallback 'parents' column if needed
-                    // This works ONLY because 'parents' is the third named axis
-                // (see __CHART_AXIS in gui/utils/chart_config_builder.py)
-                else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
-                    ret.parents = Array(ret.labels.length).fill("");
-                }
-                // Other axis
-                for (let i = 3; i < axisNames.length; i++) {
-                    ret[axisNames[i]] = getValue(datum, trace, i, true);
-                }
-                ret.text = getValue(datum, config.texts, idx, true);
-                ret.xaxis = config.xaxis[idx];
-                ret.yaxis = config.yaxis[idx];
-                ret.hovertext = getValue(datum, config.labels, idx, true);
-                const selPoints = getArrayValue(selected, idx, []);
-                if (selPoints?.length) {
-                    ret.selectedpoints = selPoints;
-                }
-                ret.orientation = getArrayValue(config.orientations, idx);
-                ret.line = getArrayValue(config.lines, idx);
-                ret.textposition = getArrayValue(config.textAnchors, idx);
-                const selectedMarker = getArrayValue(config.selectedMarkers, idx);
-                if (selectedMarker) {
-                    ret.selected = {marker: selectedMarker};
-                }
-                return ret as Data;
-            })
+                  const ret = {
+                      ...getArrayValue(config.options, idx, {}),
+                      type: config.types[idx],
+                      mode: config.modes[idx],
+                      name:
+                          getArrayValue(config.names, idx) ||
+                          (config.columns[trace[1]] ? getColNameFromIndexed(config.columns[trace[1]].dfid) : undefined)
+                  } as Record<string, unknown>;
+                  ret.marker = {...getArrayValue(config.markers, idx, ret.marker || {})};
+                  if (Object.keys(ret.marker as object).length) {
+                      MARKER_TO_COL.forEach((prop) => {
+                          const val = (ret.marker as Record<string, unknown>)[prop];
+                          if (typeof val === "string") {
+                              const arr = getValueFromCol(datum, val as string);
+                              if (arr.length) {
+                                  (ret.marker as Record<string, unknown>)[prop] = arr;
+                              }
+                          }
+                      });
+                  } else {
+                      delete ret.marker;
+                  }
+                  const xs = getValue(datum, trace, 0) || [];
+                  const ys = getValue(datum, trace, 1) || [];
+                  const addIndex = getArrayValue(config.addIndex, idx, true) && !ys.length;
+                  const baseX = addIndex ? Array.from(Array(xs.length).keys()) : xs;
+                  const baseY = addIndex ? xs : ys;
+                  const axisNames = config.axisNames.length > idx ? config.axisNames[idx] : ([] as string[]);
+                  if (baseX.length) {
+                      if (axisNames.length > 0) {
+                          ret[axisNames[0]] = baseX;
+                      } else {
+                          ret.x = baseX;
+                      }
+                  }
+                  if (baseY.length) {
+                      if (axisNames.length > 1) {
+                          ret[axisNames[1]] = baseY;
+                      } else {
+                          ret.y = baseY;
+                      }
+                  }
+                  const baseZ = getValue(datum, trace, 2, true);
+                  if (baseZ) {
+                      if (axisNames.length > 2) {
+                          ret[axisNames[2]] = baseZ;
+                      } else {
+                          ret.z = baseZ;
+                      }
+                  }
+                  // Hack for treemap charts: create a fallback 'parents' column if needed
+                  // This works ONLY because 'parents' is the third named axis
+                  // (see __CHART_AXIS in gui/utils/chart_config_builder.py)
+                  else if (config.types[idx] === "treemap" && Array.isArray(ret.labels)) {
+                      ret.parents = Array(ret.labels.length).fill("");
+                  }
+                  // Other axis
+                  for (let i = 3; i < axisNames.length; i++) {
+                      ret[axisNames[i]] = getValue(datum, trace, i, true);
+                  }
+                  ret.text = getValue(datum, config.texts, idx, true);
+                  ret.xaxis = config.xaxis[idx];
+                  ret.yaxis = config.yaxis[idx];
+                  ret.hovertext = getValue(datum, config.labels, idx, true);
+                  const selPoints = getArrayValue(selected, idx, []);
+                  if (selPoints?.length) {
+                      ret.selectedpoints = selPoints;
+                  }
+                  ret.orientation = getArrayValue(config.orientations, idx);
+                  ret.line = getArrayValue(config.lines, idx);
+                  ret.textposition = getArrayValue(config.textAnchors, idx);
+                  const selectedMarker = getArrayValue(config.selectedMarkers, idx);
+                  if (selectedMarker) {
+                      ret.selected = {marker: selectedMarker};
+                  }
+                  return ret as Data;
+              })
             : [];
         return lastDataPl.current;
     }, [props.figure, selected, data, config, dataKey]);
@@ -564,7 +565,7 @@ const Chart = (props: ChartProp) => {
             config.types,
             config.decimators,
             updateVarName,
-            module,
+            module
         ]
     );
 
@@ -578,8 +579,8 @@ const Chart = (props: ChartProp) => {
                 ? props.figure
                     ? index
                     : data[dataKey].tp_index
-                        ? (data[dataKey].tp_index[index] as number)
-                        : index
+                      ? (data[dataKey].tp_index[index] as number)
+                      : index
                 : 0,
         [data, dataKey, props.figure]
     );
@@ -617,7 +618,7 @@ const Chart = (props: ChartProp) => {
     return render ? (
         <Tooltip title={hover || ""}>
             <Box id={id} data-testid={props.testId} className={className} ref={plotRef}>
-                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
+                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
                     {Array.isArray(props.figure) && props.figure.length && props.figure[0].data !== undefined ? (
                         <Plot
                             data={props.figure[0].data as Data[]}