Skip to content

build(echart): Line chart#688

Merged
johbaxter merged 21 commits intodevfrom
399-echart-line-chart
Mar 20, 2025
Merged

build(echart): Line chart#688
johbaxter merged 21 commits intodevfrom
399-echart-line-chart

Conversation

@JaganSomannaKanini
Copy link
Copy Markdown
Contributor

Description

Complete working of the Line chart apart from brush event as mentioned in the ticket.

Changes Made

Fields
x-axis
y-axis

Tooltip
Ability to add/select multiple y-fields for a multi-line chart. Ex: I select age for x but can select bp1d AND bp1s for y

Events
Click event

Axis Features
Show/hide each axis
Add axis titles
Show/hide axis titles
Edit label font size of axis titles
Show tick marks for x-axis
Show tick marks for y-axis
Rotate axis values (by a degree amount)

Styling
Change color scheme
Add chart title
Curve type (how the line is formed) options of smooth, exact, step
Choose line type (solid, dashed, dotted)
Choose line weight

Features
Legend - toggle on/off
Display value labels
Customize value labels (choose position for the value label, rotate label by a certain degree, select font and font size and font color)
Toggle tooltips on/off
Ability to resize chart

How to Test

Initially run the DB to get all the data to render the graph with actual data.
Then proceed with the tools section.

Notes

@JaganSomannaKanini JaganSomannaKanini requested a review from a team as a code owner March 11, 2025 04:08
@JaganSomannaKanini JaganSomannaKanini linked an issue Mar 11, 2025 that may be closed by this pull request
23 tasks
@github-actions
Copy link
Copy Markdown

@CodiumAI-Agent /describe

@github-actions
Copy link
Copy Markdown

@CodiumAI-Agent /review

@QodoAI-Agent
Copy link
Copy Markdown

Title

build(echart): Line chart


User description

Description

Complete working of the Line chart apart from brush event as mentioned in the ticket.

Changes Made

Fields
x-axis
y-axis

Tooltip
Ability to add/select multiple y-fields for a multi-line chart. Ex: I select age for x but can select bp1d AND bp1s for y

Events
Click event

Axis Features
Show/hide each axis
Add axis titles
Show/hide axis titles
Edit label font size of axis titles
Show tick marks for x-axis
Show tick marks for y-axis
Rotate axis values (by a degree amount)

Styling
Change color scheme
Add chart title
Curve type (how the line is formed) options of smooth, exact, step
Choose line type (solid, dashed, dotted)
Choose line weight

Features
Legend - toggle on/off
Display value labels
Customize value labels (choose position for the value label, rotate label by a certain degree, select font and font size and font color)
Toggle tooltips on/off
Ability to resize chart

How to Test

Initially run the DB to get all the data to render the graph with actual data.
Then proceed with the tools section.

Notes


PR Type

  • Enhancement

Description

  • Introduce a new eChart line chart variant with full customization.

  • Add dedicated components for chart title, legend, tooltip, value labels, and axis styling.

  • Extend visualization block and menu to support “echart-line-graph” variation.

  • Update block menu configuration with a sample line chart block.


Changes walkthrough 📝

Relevant files
Enhancement
16 files
Visualization.constants.ts
Add line chart constants and styling options                         
+17/-0   
VisualizationBlock.tsx
Integrate line chart component into block rendering options
+7/-0     
VisualizationBlockMenu.tsx
Add menu support for line chart frame operations                 
+4/-0     
UpgradedVisualizationTool.tsx
Embed line chart settings into visualization tool interface
+222/-0 
CustomContextMenu.tsx
Introduce custom context menu for line chart interactions
+91/-0   
Fields.tsx
Implement fields settings for x-axis, y-axis, and tooltips in line
chart
+379/-0 
FrameOperationsLine.tsx
Provide frame operations functionalities specific to line chart
+147/-0 
Line.tsx
Render line chart with eCharts and interactive features   
+278/-0 
LineLegend.tsx
Add legend toggle functionality for line chart                     
+86/-0   
LineStyling.tsx
Include styling options for curve type, line type, and width settings
+235/-0 
LineTitle.tsx
Create customizable title component for the line chart variant
+360/-0 
LineTooltip.tsx
Enable tooltip configuration and toggle in line chart       
+100/-0 
LineValueLabel.tsx
Add custom value label settings and interactive configuration options
+650/-0 
XAxisStyling.tsx
Customize X Axis styling including title, ticks, and font size
+275/-0 
YAxisStyling.tsx
Provide Y Axis styling options with title, ticks, and font
customizations
+326/-0 
default-menu.ts
Add new block definition for the eChart line chart variant
+181/-0 
Formatting
1 files
ColorPalatteSettings.tsx
Minor formatting adjustment in custom palette settings (added top
padding)
+1/-0     

Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • @github-actions
    Copy link
    Copy Markdown

    @CodiumAI-Agent /improve

    @QodoAI-Agent
    Copy link
    Copy Markdown

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Code Duplication

    Several components repeat similar patterns—such as JSON parsing, state initialization, and option updates—which increases maintenance effort. Consider abstracting common logic into shared helper functions or custom hooks.

    import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
    import { computed } from "mobx";
    import { observer } from "mobx-react-lite";
    import { Select, styled, Switch, TextField, ToggleTabsGroup } from "@semoss/ui";
    import { PathValue } from "../../../../../types";
    import { BlockDef } from "../../../../../store";
    import { useBlockSettings } from "../../../../../hooks";
    import { getValueByPath } from "../../../../../utility";
    import { BAR_CHART_DATA, LINE_CHART_DATA } from "../../Visualization.constants";
    import { EchartVisualizationBlockDef } from "../../VisualizationBlock";
    import { EchartVisualizationBlockConfig } from "../../../../block-defaults";
    import { ColorPickerSettings } from "../../../../block-settings/shared/ColorPickerSettings";
    //styled select field to have full width
    const StyledSelect = styled(Select)(() => ({
        width: "100%",
    }));
    //a main section field with custom styling
    const StyledMainSection = styled("div")(() => ({
        display: "block",
        padding: "0.5rem",
        width: "100%",
    }));
    //a sub section field with custom styling
    const StyledSubSection = styled("div", {
        shouldForwardProp: (prop) => prop != "display" && prop != "justifyContent",
    })<{ display?: string; justifyContent?: string }>(
        ({ theme, display, justifyContent }) => ({
            width: "100%",
            paddingTop: "0.5rem",
            display: display ?? undefined,
            justifyContent: justifyContent ?? undefined,
        }),
    );
    //a text field with custom styling for full width
    const StyledTextField = styled(TextField)(({ theme }) => ({
        width: "100%",
    }));
    //Initial state of custom value labels as default values for managing and restoring
    const DEFAULT_VALUE_LABELS = {
        show: false,
        position: "top",
        rotate: "0",
        alignment: "center",
        font: "sans-serif",
        fontsize: "12",
        fontweight: "normal",
        fontcolour: "#000000",
        seriesIndex: "0",
    };
    //Customize value labels initial value
    const INITIAL_VALUE_LABELS = [];
    //Customize value labels component's key properties
    interface CustomizeValueLabelsKeys {
        show: boolean;
        position: string;
        rotate: string;
        alignment: string;
        font: string;
        fontsize: string;
        fontweight: string;
        fontcolour: string;
        seriesIndex: number | string;
    }
    //having custom fields to customize charts text parts like: position, alignment, rotate, etc
    export const LineValueLabels = observer(
        <D extends BlockDef = BlockDef>({ option, chartType, id, path }) => {
            const [fieldData, setFieldData] =
                useState<CustomizeValueLabelsKeys[]>(INITIAL_VALUE_LABELS);
            const { data, setData } =
                useBlockSettings<EchartVisualizationBlockDef>(id);
            const [value, setValue] = useState<
                typeof EchartVisualizationBlockConfig.data.option
            >(data.option);
            const [selectedSeries, setSelectedSeries] = useState<string>("0");
            const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
            const [valueLabelsUpdated, setValueLabelsUpdated] = useState<
                "initial" | "updated"
            >("initial");
            const labelPositionValues: string[] = [
                "top",
                "left",
                "right",
                "bottom",
                "inside",
                "insideLeft",
                "insideRight",
                "insideTop",
                "insideBottom",
                "insideTopLeft",
                "insideBottomLeft",
                "insideTopRight",
                "insideBottomRight",
            ];
            const alignment: string[] = ["left", "center", "right"];
            const fontFamily: string[] = ["sans-serif", "serif", "monospace"];
            const fontWeight: string[] = [
                "normal",
                "bold",
                "bolder",
                "lighter",
                "100",
                "200",
                "300",
                "400",
                "500",
                "600",
                "700",
                "800",
                "900",
            ];
            //for retaining the previously selected values, this useeffect will help
            useEffect(() => {
                let option = typeof value === "string" ? JSON.parse(value) : value;
                if (option["series"]) {
                    let seriesData = getFilteredSeriesIndex();
                    let fieldsData = fieldData;
                    let fieldsDataToUpdate = seriesData.map((seriesChartData) => {
                        if (
                            option["series"][seriesChartData]["label"] === undefined
                        ) {
                            return {
                                ...DEFAULT_VALUE_LABELS,
                                seriesIndex: seriesChartData,
                            };
                        } else {
                            return {
                                show:
                                    option["series"][seriesChartData]["label"]
                                        .show ?? false,
                                position:
                                    option["series"][seriesChartData]["label"]
                                        .position ?? DEFAULT_VALUE_LABELS.position,
                                rotate:
                                    option["series"][seriesChartData]["label"]
                                        .rotate ?? DEFAULT_VALUE_LABELS.rotate,
                                alignment:
                                    option["series"][seriesChartData]["label"]
                                        .align ?? DEFAULT_VALUE_LABELS.alignment,
                                font:
                                    option["series"][seriesChartData]["label"]
                                        .fontFamily ?? DEFAULT_VALUE_LABELS.font,
                                fontsize:
                                    option["series"][seriesChartData]["label"]
                                        .fontSize ?? DEFAULT_VALUE_LABELS.fontsize,
                                fontweight:
                                    option["series"][seriesChartData]["label"]
                                        .fontWeight ??
                                    DEFAULT_VALUE_LABELS.fontweight,
                                fontcolour:
                                    option["series"][seriesChartData]["label"]
                                        .color ?? DEFAULT_VALUE_LABELS.fontcolour,
                                seriesIndex: seriesChartData,
                            };
                        }
                    });
                    setFieldData((prevFieldsData) => {
                        return fieldsDataToUpdate;
                    });
                }
            }, []);
            // get the value of the input (wrapped in usememo because of path prop)
            const computedValue = useMemo(() => {
                return computed(() => {
                    if (!data) {
                        return "";
                    }
                    const v = getValueByPath(data, path);
                    if (typeof v === "undefined") {
                        return "";
                    } else if (typeof v === "string") {
                        return v;
                    }
                    return JSON.stringify(v, null, 2);
                });
            }, [data, path]).get();
            //updating local 'value' state to the most recent state
            useEffect(() => {
                setValue(computedValue);
            }, [computedValue]);
            //update the chart data to state, when any of customize value labels field is changed
            useEffect(() => {
                if (valueLabelsUpdated === "updated") {
                    updateChartData(fieldData);
                }
            }, [fieldData]);
            //handles different input fields by setting values to state, whenever a change happens
            const updateFields = (
                fieldName,
                fieldValue,
                fieldType,
                seriesIndex,
            ): void => {
                if (valueLabelsUpdated === "initial") {
                    setValueLabelsUpdated("updated");
                }
                let fieldsData = fieldData;
                fieldsData[seriesIndex] = {
                    ...fieldsData[seriesIndex],
                    [fieldName]:
                        fieldType === "switch"
                            ? fieldValue.target.checked
                            : fieldValue.target.value,
                };
                setFieldData((prevData) => {
                    return [...fieldsData];
                });
            };
            //update the chart data to state, when customize value labels fields section is updated to new value
            function updateChartData(values: CustomizeValueLabelsKeys[]) {
                let option = typeof value === "string" ? JSON.parse(value) : value;
                let optionUpdated = option;
                let customizeLabelOptionsData = {};
                values.forEach((item) => {
                    customizeLabelOptionsData[item.seriesIndex] = {
                        show: item.show,
                        position: item.position,
                        rotate: item.rotate,
                        alignment: item.alignment,
                        font: item.font,
                        fontsize: item.fontsize,
                        fontweight: item.fontweight,
                        fontcolour: item.fontcolour,
                    };
                });
                const customizeLabelOptionsValue = customizeLabelOptionsData;
                //get matching series index for bar chart
                const filteredSeries: number[] = getFilteredSeriesIndex();
                //update the series with new styles for every matching series index
                filteredSeries.forEach((item) => {
                    const displayPositionIndex: number = item;
                    let showValueLabel: boolean =
                        customizeLabelOptionsValue[displayPositionIndex]["show"] ??
                        false;
                    if (customizeLabelOptionsValue[displayPositionIndex]["show"]) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex]["position"]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["position"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["position"],
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex]["rotate"]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["rotate"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["rotate"],
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex][
                            "alignment"
                        ]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["align"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["alignment"],
                                },
                            };
                        }
                    }
                    if (customizeLabelOptionsValue[displayPositionIndex]["font"]) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["fontFamily"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["font"],
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex]["fontsize"]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["fontSize"]:
                                        Number(
                                            customizeLabelOptionsValue[
                                                displayPositionIndex
                                            ]["fontsize"],
                                        ) || undefined,
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex][
                            "fontweight"
                        ]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["fontWeight"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["fontweight"],
                                },
                            };
                        }
                    }
                    if (
                        customizeLabelOptionsValue[displayPositionIndex][
                            "fontcolour"
                        ]
                    ) {
                        if (option["series"][displayPositionIndex]) {
                            option["series"][displayPositionIndex] = {
                                ...option["series"][displayPositionIndex],
                                ["label"]: {
                                    ...option["series"][displayPositionIndex][
                                        "label"
                                    ],
                                    ["show"]: showValueLabel,
                                    ["color"]:
                                        customizeLabelOptionsValue[
                                            displayPositionIndex
                                        ]["fontcolour"] ||
                                        option["series"][displayPositionIndex][
                                            "label"
                                        ]["color"],
                                },
                            };
                        }
                    }
                });
                option = {
                    ...option,
                    ["customSettings"]: {
                        ["toolsUpdated"]: true,
                    },
                };
                optionUpdated = option;
                runStateUpdateCustom(optionUpdated);
            }
            //function to check and retrieve the indexes for bar chart type
            function getFilteredSeriesIndex() {
                let index = [];
                let option = typeof value === "string" ? JSON.parse(value) : value;
                let seriesAvailable = option["series"].filter((item) =>
                    LINE_CHART_DATA.JSONVALUE.includes(item.type),
                );
                seriesAvailable.forEach((item, seriesIndex) => {
                    index.push(seriesIndex);
                });
                return index;
            }
            //update the state when any of the fields in custom value labels is changed
            function runStateUpdateCustom(
                optionUpdated: typeof EchartVisualizationBlockConfig.data.option,
            ) {
                if (timeoutRef.current) {
                    clearTimeout(timeoutRef.current);
                    timeoutRef.current = null;
                }
                timeoutRef.current = setTimeout(() => {
                    try {
                        setData(
                            "option",
                            optionUpdated as PathValue<D["data"], typeof path>,
                        );
                    } catch (e) {
                        console.log(e);
                    }
                }, 300);
            }
    
            const fieldSelectedSeries: CustomizeValueLabelsKeys =
                fieldData[parseInt(selectedSeries)] || DEFAULT_VALUE_LABELS;
    
            const getAccordianDetails = (
                <StyledMainSection>
                    <StyledSubSection display="flex" justifyContent="space-between">
                        <ToggleTabsGroup
                            onChange={(e: React.SyntheticEvent, val: string) =>
                                setSelectedSeries((prevSelectedSeries) => val)
                            }
                            value={selectedSeries}
                        >
                            {fieldData.length &&
                                fieldData.map((item, index) => {
                                    return (
                                        <ToggleTabsGroup.Item
                                            label={`Series ${index + 1}`}
                                            value={`${index}`}
                                            key={`series${index}`}
                                        />
                                    );
                                })}
                            ;
                        </ToggleTabsGroup>
                    </StyledSubSection>
                    {parseInt(selectedSeries) >= 0 && (
                        <StyledSubSection
                            display="flex"
                            justifyContent="space-between"
                        >
                            <Switch
                                checked={fieldSelectedSeries?.show ?? undefined}
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
                                    updateFields(
                                        "show",
                                        e,
                                        "switch",
                                        selectedSeries,
                                    )
                                }
                                title="Show Value Labels"
                            />
                            <label htmlFor="show-value-labels">
                                Show Value Labels
                            </label>
                        </StyledSubSection>
                    )}
                    {fieldSelectedSeries?.show && (
                        <>
                            <StyledSubSection>
                                <label htmlFor="label-position">Position</label>
                                <StyledSelect
                                    id="label-position"
                                    value={fieldSelectedSeries.position ?? ""}
                                    onChange={(e) =>
                                        updateFields(
                                            "position",
                                            e,
                                            "select",
                                            selectedSeries,
                                        )
                                    }
                                >
                                    <Select.Item key="-1" value="">
                                        Select
                                    </Select.Item>
                                    {labelPositionValues.map((label, index) => {
                                        return (
                                            <Select.Item value={label} key={index}>
                                                {label}
                                            </Select.Item>
                                        );
                                    })}
                                </StyledSelect>
                            </StyledSubSection>
                            <StyledSubSection>
                                <label htmlFor="rotate-label">
                                    Rotate Label(In Degrees)
                                </label>
                                <StyledTextField
                                    variant={"outlined"}
                                    type="number"
                                    id="rotate-label"
                                    value={fieldSelectedSeries.rotate ?? ""}
                                    onChange={(e) =>
                                        updateFields(
                                            "rotate",
                                            e,
                                            "text",
                                            selectedSeries,
                                        )
                                    }
                                ></StyledTextField>
                            </StyledSubSection>
                            <StyledSubSection>
                                <label htmlFor="alignment-label">
                                    Select Alignment
                                </label>
                                <StyledSelect
                                    id="alignment-label"
                                    value={fieldSelectedSeries.alignment ?? ""}
                                    onChange={(e) =>
                                        updateFields(
                                            "alignment",
                                            e,
                                            "select",
                                            selectedSeries,
                                        )
                                    }
                                >
                                    <Select.Item key="-1" value="">
                                        Select Alignment
                                    </Select.Item>
                                    {alignment.map((label, index) => {
                                        return (
                                            <Select.Item value={label} key={index}>
                                                {label}
                                            </Select.Item>
                                        );
                                    })}
                                </StyledSelect>
                            </StyledSubSection>
                            <StyledSubSection>
                                <label htmlFor="font">Select Font</label>
                                <StyledSelect
                                    id="font"
                                    value={fieldSelectedSeries.font ?? ""}
                                    onChange={(e) =>
                                        updateFields(
                                            "font",
                                            e,
                                            "select",
                                            selectedSeries,
                                        )
                                    }
                                >
                                    <Select.Item key="-1" value="">
                                        Select Font
                                    </Select.Item>
                                    {fontFamily.map((label, index) => {
                                        return (
                                            <Select.Item value={label} key={index}>
                                                {label}
                                            </Select.Item>
                                        );
                                    })}
                                </StyledSelect>
                            </StyledSubSection>
                            <StyledSubSection>
                                <label htmlFor="font-size">
                                    Select Font Size (Default: 12)
                                </label>
                                <StyledTextField
                                    variant={"outlined"}
                                    type="number"
                                    id="font-size"
                                    // defaultValue={fieldData.fontsize}
                                    value={fieldSelectedSeries.fontsize}
                                    onChange={(e) =>
                                        updateFields(
                                            "fontsize",
                                            e,
                                            "text",
                                            selectedSeries,
                                        )
                                    }
                                ></StyledTextField>
                            </StyledSubSection>
                            <StyledSubSection>
                                <label htmlFor="font-weight">
                                    Select Font Weight
                                </label>
                                <StyledSelect
                                    id="font-weight"
                                    value={fieldSelectedSeries.fontweight}
                                    onChange={(e) =>
                                        updateFields(
                                            "fontweight",
                                            e,
                                            "select",
                                            selectedSeries,
                                        )
                                    }
                                >
                                    <Select.Item key="-1" value="">
                                        Select Font Weight
                                    </Select.Item>
                                    {fontWeight.map((label, index) => {
                                        return (
                                            <Select.Item value={label} key={index}>
                                                {label}
                                            </Select.Item>
                                        );
                                    })}
                                </StyledSelect>
                            </StyledSubSection>
                            <StyledSubSection>
                                <ColorPickerSettings
                                    id={id}
                                    path={`option.series.${selectedSeries}.label.color`}
                                    colorValue={fieldSelectedSeries.fontcolour}
                                    onChange={(e) =>
                                        updateFields(
                                            "fontcolour",
                                            { target: { value: e } },
                                            "text",
                                            selectedSeries,
                                        )
                                    }
                                />
                            </StyledSubSection>
                        </>
                    )}
                    <br />
                </StyledMainSection>
            );
    
            return <>{getAccordianDetails}</>;
        },
    );
    Error Handling

    Directly calling JSON.parse without try/catch in several components may result in runtime errors if the JSON data is malformed. Incorporate proper error handling and validation to improve robustness.

    import { useBlockSettings } from "../../../../../hooks";
    import { Block, BlockDef } from "../../../../../store";
    import { Paths, PathValue } from "../../../../../types";
    import { getValueByPath } from "../../../../../utility";
    import { styled, Switch, TextField, Select, Button } from "@semoss/ui";
    import { computed } from "mobx";
    import { observer } from "mobx-react-lite";
    import { ChangeEvent, useEffect, useMemo, useState } from "react";
    interface JsonSettingsProps<D extends BlockDef = BlockDef> {
        /**
         * Id of the block that is being worked with
         */
        id: string;
    
        path: Paths<Block<D>["data"], 4>;
    }
    const StyledAxisDiv = styled("div")<{
        display?: string;
        justifyContent?: string;
    }>(({ theme, display, justifyContent }) => ({
        display: display ?? undefined,
        justifyContent: justifyContent ?? undefined,
        flexDirection: "row",
        padding: "0.5rem",
    }));
    const StyledButton = styled(Button)({
        left: "80%",
    });
    const StyledAxisColDiv = styled("div")<{
        display?: string;
        justifyContent: string;
    }>(({ theme, display, justifyContent }) => ({
        display: display ?? undefined,
        justifyContent: justifyContent ?? undefined,
        flexDirection: "column",
        padding: "0.5rem",
    }));
    const StyledTextField = styled(TextField)(({ theme }) => ({
        width: "100%",
    }));
    const StyledLabel = styled("label")(({ theme }) => ({
        paddingLeft: "10px",
    }));
    export const YAxisStyling = observer(
        <D extends BlockDef = BlockDef>({ id, path }: JsonSettingsProps<D>) => {
            const { data, setData } = useBlockSettings<D>(id);
            const [value, setValue] = useState("");
            const [showYAxis, setShowYAxis] = useState(true);
            const [showYAxisTitle, setShowYAxisTitle] = useState(true);
            const [showYAxisTick, setShowYAxisTick] = useState(true);
            const [yAxisTitle, setYAxisTitle] = useState("");
            const [yAxisFont, setYAxisFont] = useState(10);
            const computedValue = useMemo(() => {
                return computed(() => {
                    if (!data) {
                        return "";
                    }
                    const v = getValueByPath(data, path);
                    if (typeof v === "undefined") {
                        return "";
                    } else if (typeof v === "string") {
                        return v;
                    }
                    return JSON.stringify(v, null, 2);
                });
            }, [data, path]).get();
            useEffect(() => {
                setValue(computedValue);
            }, [computedValue, data]);
            useEffect(() => {
                if (data.hasOwnProperty("option")) {
                    reInitializeFeatures(data.option);
                }
            }, [id]);
            useEffect(() => {
                if (data.hasOwnProperty("option")) {
                    retainLocalState(data.option);
                }
            }, [showYAxis]);
            /**
             * Retains the local state of the feature on toggle switch and on reset button
             * With the local state we will be displaying the values in the fields
             * @param options the options passed in when the chart is loaded
             */
            const retainLocalState = (options) => {
                //Retain the local state of the y axis visible
                setShowYAxis(options["yAxis"].show);
                //Retain the local state of the y axis title
                setShowYAxisTitle(options["yAxis"]["show"]);
                //Check if the y axis name is null
                if (options["reset"]["yAxis"]["updatedName"] === null) {
                    //Set the y axis title to the name in the option
                    setYAxisTitle(options["yAxis"]["name"]);
                } else {
                    if (options["reset"]["yAxis"].hasOwnProperty("updatedName")) {
                        //Set the y axis title to the updated name in the reset option
                        setYAxisTitle(options["reset"]["yAxis"]["updatedName"]);
                        //Update the y axis name in the option
                        options["yAxis"]["name"] =
                            options["reset"]["yAxis"]["updatedName"];
                        //Update the data
                        setData(path, options as PathValue<D["data"], typeof path>);
                    } else {
                        let yAxisNames = options["_state"]["fields"]["yAxis"];
                        setYAxisTitle(
                            options["_state"]["fields"]["yAxis"].join(","),
                        );
                        for (let i = 0; i < yAxisNames.length; i++) {
                            options["series"][i]["name"] = yAxisNames[i];
                        }
                        options["yAxis"]["name"] = yAxisNames;
                        setData(path, options as PathValue<D["data"], typeof path>);
                    }
                }
                //Retain the local state of the y axis tick
                setShowYAxisTick(options["yAxis"]["axisTick"].show);
                //Retain the local state of the y axis font size
                setYAxisFont(options["yAxis"]["nameTextStyle"]["fontSize"]);
            };
            //Reinitialize the feature when the chart is loaded
            const reInitializeFeatures = (options) => {
                setShowYAxis(options["yAxis"].show);
            };
            /**
             * Handle the change event for any Value Label input
             * @param title the title of the input field
             * @param inputValue the value of the input field
             */
            function handleInputChange(title: string, inputValue) {
                let option = JSON.parse(value);
                /**
                 * Update the showYAxis property of the option
                 * @param inputValue boolean value indicating whether to show the y axis
                 */
                if (title === "showYAxis") {
                    option["yAxis"].show = inputValue;
                    setShowYAxis(inputValue);
                } else if (title === "showYAxisTitle") {
                    /**
                     * Update the showYAxisTitle property of the option
                     * @param inputValue boolean value indicating whether to show the y axis title
                     */
                    if (inputValue) {
                        let tilteNames =
                            option["reset"]["yAxis"]["updatedName"].split(",");
                        option["yAxis"]["name"] = tilteNames;
                        let dataLength = option["series"].length;
                        if (dataLength > 0) {
                            for (let i = 0; i < dataLength; i++) {
                                option["series"][i]["name"] = tilteNames[i];
                            }
                        }
                        // option['reset']['yAxis']['updatedName'] = yAxisTitle;
                    } else {
                        option["yAxis"]["name"] = "";
                        let dataLength = option["series"].length;
                        if (dataLength > 0) {
                            for (let i = 0; i < dataLength; i++) {
                                option["series"][i]["name"] = "";
                            }
                        }
                    }
                    setShowYAxisTitle(inputValue);
                } else if (title === "yAxisTitle") {
                    /**
                     * Update the yAxisTitle property of the option
                     * @param inputValue string value indicating the title of the y axis
                     */
                    setYAxisTitle(inputValue);
                    let tilteNames = inputValue.split(",");
                    option["yAxis"]["name"] = tilteNames;
                    let dataLength = option["series"].length;
                    if (dataLength > 0) {
                        for (let i = 0; i < dataLength; i++) {
                            option["series"][i]["name"] = tilteNames[i];
                        }
                    }
                    option["reset"]["yAxis"]["updatedName"] = inputValue;
                } else if (title === "showYAxisTick") {
                    /**
                     * Update the showYAxisTick property of the option
                     * @param inputValue boolean value indicating whether to show the y axis tick
                     */
                    option["yAxis"]["axisTick"].show = inputValue;
                    setShowYAxisTick(inputValue);
                } else if (title === "yAxisFont") {
                    /**
                     * Update the yAxisFont property of the option
                     * @param inputValue number value indicating the font size of the y axis
                     */
                    option["yAxis"]["nameTextStyle"]["fontSize"] = inputValue;
                    setYAxisFont(inputValue);
                }
                setData(path, option as PathValue<D["data"], typeof path>);
            }
            /**
             * Resets the Y Axis settings to their default values as specified
             * in the 'reset' object of the current option.
             */
            function handleReset() {
                // Parse the current option value from the stored JSON string
                let option = JSON.parse(value);
                // Reset Y Axis tick visibility to default
                setShowYAxisTick(option["reset"]["yAxis"]["axisTick"]);
                // Reset Y Axis font size to default
                setYAxisFont(option["reset"]["yAxis"]["axisLabelFont"]);
                // Get the Y Axis name from the local state or set it to an empty string if not available
                let yaxisName =
                    option["_state"] === undefined
                        ? ""
                        : option["_state"]["fields"]["yAxis"].join(",");
                // Update the Y Axis name in the option
                option["yAxis"]["name"] = option["_state"]["fields"]["yAxis"];
                // Set the Y Axis title
                setYAxisTitle(yaxisName);
                // Update the Y Axis title in the reset option
                option["reset"]["yAxis"]["updatedName"] = yaxisName;
                // Ensure Y Axis title is set to be shown
                setShowYAxisTitle(true);
                // Update the Y Axis tick visibility in the option
                option["yAxis"]["axisTick"]["show"] =
                    option["reset"]["yAxis"]["axisTick"];
                // Update the Y Axis font size in the option
                option["yAxis"]["nameTextStyle"]["fontSize"] =
                    option["reset"]["yAxis"]["axisLabelFont"];
                // Split the Y Axis name into individual titles and update the series names accordingly
                let tilteNames = yaxisName.split(",");
                let dataLength = option["series"].length;
                if (dataLength > 0) {
                    for (let i = 0; i < dataLength; i++) {
                        option["series"][i]["name"] = tilteNames[i];
                    }
                }
                // Update the data with the new option
                setData(path, option as PathValue<D["data"], typeof path>);
            }
            return (
                <StyledAxisDiv>
                    <StyledAxisDiv display="flex" justifyContent="flex-start">
                        <Switch
                            checked={showYAxis}
                            onChange={(e: ChangeEvent<HTMLInputElement>) =>
                                handleInputChange("showYAxis", e.target.checked)
                            }
                            title="Show Y Axis"
                        />
                        <StyledLabel>Show Y Axis</StyledLabel>
                    </StyledAxisDiv>
                    {showYAxis && (
                        <StyledAxisDiv display="flex" justifyContent="flex-start">
                            <Switch
                                checked={showYAxisTitle}
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
                                    handleInputChange(
                                        "showYAxisTitle",
                                        e.target.checked,
                                    )
                                }
                                title="Show Y Axis Title"
                            />
                            <StyledLabel>Show Y Axis Title</StyledLabel>
                        </StyledAxisDiv>
                    )}
                    {showYAxis && showYAxisTitle && (
                        <StyledAxisColDiv
                            display="flex"
                            justifyContent="space-around"
                        >
                            <label htmlFor="x-axis-title">Edit Y Axis Title</label>
                            <StyledTextField
                                id="yAxisTitle"
                                name="yAxisTitle"
                                value={yAxisTitle}
                                onChange={(e) =>
                                    handleInputChange("yAxisTitle", e.target.value)
                                }
                            />
                        </StyledAxisColDiv>
                    )}
                    {showYAxis && (
                        <StyledAxisDiv display="flex" justifyContent="flex-start">
                            <Switch
                                checked={showYAxisTick}
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
                                    handleInputChange(
                                        "showYAxisTick",
                                        e.target.checked,
                                    )
                                }
                                title="Show Y Axis Tick"
                            />
                            <StyledLabel>Show Y Axis Tick</StyledLabel>
                        </StyledAxisDiv>
                    )}
                    {showYAxis && (
                        <StyledAxisColDiv
                            display="flex"
                            justifyContent="space-around"
                        >
                            <label htmlFor="label-font">
                                YAxis Label Font Size
                            </label>
                            <StyledTextField
                                id="labelfont"
                                name="labelfont"
                                value={yAxisFont}
                                onChange={(e) =>
                                    handleInputChange("yAxisFont", e.target.value)
                                }
                            />
                        </StyledAxisColDiv>
                    )}
                    {showYAxis && (
                        <StyledButton
                            variant="contained"
                            color="primary"
                            size="small"
                            onClick={handleReset}
                        >
                            Reset
                        </StyledButton>
                    )}
                </StyledAxisDiv>
            );
        },
    );

    @JaganSomannaKanini JaganSomannaKanini mentioned this pull request Mar 11, 2025
    23 tasks
    @QodoAI-Agent
    Copy link
    Copy Markdown

    PR Code Suggestions ✨

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Handle titleColor update

    Add a new branch in handleInputChange to process "titleColor" so that the
    ColorPickerSettings onChange event properly updates the title color in the option.

    libs/renderer/src/components/block-defaults/echart-visualization-block/variant/line-chart/LineTitle.tsx [128-169]

     function handleInputChange(title: string, inputValue) {
         let option = JSON.parse(value);
         if (title === "showTitle") {
    -        // Update the showTitle property of the option
             option["title"].show = inputValue;
             setShowTitle(inputValue);
         } else if (title === "titleName") {
    -        // Update the titleName property of the option
             option["title"]["text"] = inputValue;
             setTitle((prev) => ({ ...prev, name: inputValue }));
    +    } else if (title === "titleAlignment") {
    +        option["title"]["left"] = inputValue;
    +        setTitle((prev) => ({ ...prev, alignment: inputValue }));
    +    } else if (title === "titleSize") {
    +        option["title"]["textStyle"]["fontSize"] = inputValue;
    +        setTitle((prev) => ({ ...prev, size: inputValue }));
    +    } else if (title === "titleWeight") {
    +        option["title"]["textStyle"]["fontWeight"] = inputValue;
    +        setTitle((prev) => ({ ...prev, weight: inputValue }));
    +    } else if (title === "titleFamily") {
    +        option["title"]["textStyle"]["fontFamily"] = inputValue;
    +        setTitle((prev) => ({ ...prev, family: inputValue }));
    +    } else if (title === "titleColor") {
    +        option["title"]["textStyle"]["color"] = inputValue;
    +        setTitle((prev) => ({ ...prev, color: inputValue }));
         }
    -    // ... remaining branches for titleAlignment, titleSize, titleWeight, titleFamily
         setData(path, option as PathValue<D["data"], typeof path>);
     }
    Suggestion importance[1-10]: 9

    __

    Why: This suggestion correctly adds a branch for handling the "titleColor" update, ensuring that changes from the ColorPickerSettings are properly reflected in the state. It directly addresses a missing update in the PR and improves functionality significantly.

    High
    Correct tooltip removal mapping

    Refactor the map callback to properly remove properties without returning undefined.

    libs/renderer/src/components/block-defaults/echart-visualization-block/variant/line-chart/Fields.tsx [181-186]

    -delete tempValue["tooltip"]["formatter"];
     tempValue["series"][0] = {
         ...tempValue["series"][0],
    -    data: tempValue["series"][0]["data"].map(
    -        (x) => delete x[removedChips],
    -    ),
    +    data: tempValue["series"][0]["data"].map(x => {
    +        delete x[removedChips];
    +        return x;
    +    }),
     };
    Suggestion importance[1-10]: 8

    __

    Why: The improved code refactors the map callback to delete a property from each object without returning undefined, thereby fixing a logical bug that could disrupt the expected data structure.

    Medium
    Add JSON parse error handling

    Wrap the JSON.parse call in a try-catch block to prevent runtime errors if the JSON
    in value is invalid.

    libs/renderer/src/components/block-defaults/echart-visualization-block/variant/line-chart/LineStyling.tsx [128-169]

     function handleInputChange(line: string, inputValue) {
    -    let option = JSON.parse(value);
    +    let option;
    +    try {
    +        option = JSON.parse(value);
    +    } catch (e) {
    +        console.error("Failed to parse JSON value:", e);
    +        return;
    +    }
         if (line === "lineCurve") {
             // code updating series curve type
         } else if (line === "lineType") {
             // code updating line style type
         } else if (line === "lineWidth") {
             // code updating line style width
         }
         setData(path, option as PathValue<D["data"], typeof path>);
     }
    Suggestion importance[1-10]: 7

    __

    Why: This suggestion introduces a try-catch around JSON.parse to avoid potential runtime errors when the JSON is invalid. While it is a sound defensive programming measure, its impact is moderate given the expected format of the data.

    Medium
    Fix axis naming typos

    Rename mispelled keys to use "XAxis" and "YAxis" for clarity and consistency.

    libs/renderer/src/components/block-defaults/echart-visualization-block/UpgradedVisualizationTool.tsx [877-898]

     {data.variation === "echart-line-graph" && (
         <ListItemButton
             onClick={(e) =>
                 setSelectedList((prevList) =>
    -                prevList === "lineXAixsStyling"
    +                prevList === "lineXAxisStyling"
                         ? ""
    -                    : "lineXAixsStyling",
    +                    : "lineXAxisStyling",
                 )
             }
    -        selected={selectedList === "lineXAixsStyling"}
    +        selected={selectedList === "lineXAxisStyling"}
         >
             <ListItemText primary="X Axis Styling" />
             <InfoOutlined />
         </ListItemButton>
     )}
    Suggestion importance[1-10]: 5

    __

    Why: The suggestion addresses a typo by renaming "lineXAixsStyling" to "lineXAxisStyling" for clarity, which is a minor but useful improvement for code readability.

    Low

    @ehynd
    Copy link
    Copy Markdown
    Contributor

    ehynd commented Mar 11, 2025

    @JaganSomannaKanini two changes from me:
    Under Edit Y and Edit X axis, could you please add in rotate axis values? Ex: If I had values 1-10 along the x-axis, I could select the degrees that I wanted to rotate. This is included in bar chart to reference!

    Could you please take a look at Show Title under Y-Axis styling? When I have data within the chart, if I turn the Y axis title off, the chart goes blank. I also am unable to then use the switch to turn the title back on as well.

    @QodoAI-Agent
    Copy link
    Copy Markdown

    PR Code Suggestions ✨

    No code suggestions found for the PR.

    @ehynd
    Copy link
    Copy Markdown
    Contributor

    ehynd commented Mar 17, 2025

    @JaganSomannaKanini after raising PR for CRUD, could you please finish addressing these as next priority?

    @JaganSomannaKanini
    Copy link
    Copy Markdown
    Contributor Author

    @ehynd Sure, after raising PR for CRUD ticket I will take a look on this as priority.

    @johbaxter johbaxter merged commit 3849f19 into dev Mar 20, 2025
    3 checks passed
    @johbaxter johbaxter deleted the 399-echart-line-chart branch March 20, 2025 13:54
    @github-actions
    Copy link
    Copy Markdown

    @CodiumAI-Agent /update_changelog

    @QodoAI-Agent
    Copy link
    Copy Markdown

    Changelog updates: 🔄

    2025-03-20

    Added

    • Echart based line chart with customizable axes, title, legend, tooltips, and value labels.
    • New UI panels and configuration options for comprehensive line chart styling and interactions.

    to commit the new content to the CHANGELOG.md file, please type:
    '/update_changelog --pr_update_changelog.push_changelog_changes=true'

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    Create Line Chart

    5 participants