diff --git a/README.md b/README.md index cd8b6bb..15a35b1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ComfyUI was designed to make Stable Diffusion accessible to non-engineers while At the end of this page, you’ll find a tutorial on how to use this package to answer the MLB hits question. -I hope you enjoy using it and have a great time exploring data. +I hope you find it useful and engaging for data exploration. — Hide Inada @@ -36,7 +36,7 @@ Here are just some of the included nodes. For a complete list of nodes, check ou You can connect these nodes to conduct complex analysis in an intuitive graphical way. You can also use other Comfy custom nodes to fit your needs (e.g., combining images). -These custom nodes aim to make data exploration and analysis more efficient and enjoyable. +These custom nodes enhance efficiency and usability in data exploration and analysis. ## Installation Note: GPU is not required to run this custom module. @@ -58,7 +58,7 @@ Note: GPU is not required to run this custom module. ``` This will create a new subdirectory ComfyUI-Data-Analysis. 3. Rename the folder name from ComfyUI-Data-Analysis to data-analysis. - If you skip this, example workflows will not work as those flows use this folder name. + Skipping this step will cause example workflows to fail, as they rely on this folder name. 4. Check if your ComfyUI environment already has pandas and matplotlib. If not, install them using pip. ``` pip install -r requirements.txt @@ -75,7 +75,7 @@ Refer to [User's Guide](docs/ug.md) or check out [Node Reference](docs/reference For a practical example, see the tutorial on baseball data: [How to Find Out Who Had the Most Hits](docs/baseball_example1.md). ## Update -Please refer to the release notes. I try to avoid backward-incompatible changes as much as possible, but sometimes they are unavoidable because the project is still in its early stage and I discover issues along the way. The release notes contain those changes so that you know what to expect before upgrading your version. +Refer to the release notes. While backward-incompatible changes are minimized, some may be necessary. The release notes contain those changes so that you know what to expect before upgrading your version. ## Contribution 🚫 **This project does not accept pull requests.** diff --git a/__init__.py b/__init__.py index a0e2d60..d15761f 100644 --- a/__init__.py +++ b/__init__.py @@ -105,6 +105,9 @@ from .modules.pandas_wrapper.pandas_var import PandasVar from .modules.pandas_wrapper.pandas_vertical_concat import PandasVerticalConcat from .modules.pandas_wrapper.pandas_vertical_split import PandasVerticalSplit from .modules.pandas_wrapper.pandas_xs import PandasXs +from .modules.seaborn_wrapper.sns_bar import SNSBar +from .modules.seaborn_wrapper.sns_line import SNSLine +from .modules.seaborn_wrapper.sns_scatter import SNSScatter T = TypeVar("T") @@ -219,6 +222,9 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasVerticalConcat": PandasVerticalConcat, "PandasVerticalSplit": PandasVerticalSplit, "PandasXs": PandasXs, + "SNSBar": SNSBar, + "SNSLine": SNSLine, + "SNSScatter": SNSScatter, } @@ -333,6 +339,9 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasVerticalConcat": "Pandas Vertical Concat", "PandasVerticalSplit": "Pandas Vertical Split", "PandasXs": "Pandas Xs", + "SNSBar": "SNS Bar Chart", + "SNSLine": "SNS Line Plot", + "SNSScatter": "SNS Scatter Plot", } diff --git a/dev/scripts/init_py_generate.py b/dev/scripts/init_py_generate.py index fa4d26d..2d22740 100644 --- a/dev/scripts/init_py_generate.py +++ b/dev/scripts/init_py_generate.py @@ -76,12 +76,16 @@ def format_display_name(class_name): ("CD A", "CDA"), ("H T M L", "HTML"), ("MP L", "MPL"), + ("SN S", "SNS"), (" N A", " NA"), ("Data Frame", "DataFrame"), ("Multi Index", "MultiIndex"), ("MPL Bar", "MPL Bar Chart"), ("MPL Line", "MPL Line Plot"), - ("MPL Scatter", "MPL Scatter Plot") + ("MPL Scatter", "MPL Scatter Plot"), + ("SNS Bar", "SNS Bar Chart"), + ("SNS Line", "SNS Line Plot"), + ("SNS Scatter", "SNS Scatter Plot") ] for p in replace_pairs: final_result = final_result.replace(p[0], p[1]) diff --git a/docs/images/sns_line.png b/docs/images/sns_line.png new file mode 100644 index 0000000..701f559 Binary files /dev/null and b/docs/images/sns_line.png differ diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index 16e06a1..ea18e87 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -114,6 +114,9 @@ | [MPL Bar Chart](mpl_bar.md) | Generates a bar chart from a Pandas DataFrame. | | [MPL Line Plot](mpl_line.md) | Generates a line plot from a pandas DataFrame. | | [MPL Scatter Plot](mpl_scatter.md) | Generates a scatter plot from a pandas DataFrame. | +| [SNS Bar Chart](sns_bar.md) | Generates a bar chart from a Pandas DataFrame using Seaborn. | +| [SNS Line Plot](sns_line.md) | Generates a line plot from a pandas DataFrame using Seaborn. | +| [SNS Scatter Plot](sns_scatter.md) | Generates a scatter plot from a pandas DataFrame using Seaborn. | ## Summary statistics | Node | Description | | --- | --- | diff --git a/docs/reference/sns_bar.md b/docs/reference/sns_bar.md new file mode 100644 index 0000000..4fd2db7 --- /dev/null +++ b/docs/reference/sns_bar.md @@ -0,0 +1,14 @@ +# SNS Bar Chart +Generates a bar chart from a Pandas DataFrame using Seaborn. + +For style and palette parameters, refer to [SNS Line Plot documentation](sns_line.md). + +## Output +| Data type | +|---| +| Image | + +
+Category: Plot + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/sns_line.md b/docs/reference/sns_line.md new file mode 100644 index 0000000..4c476bc --- /dev/null +++ b/docs/reference/sns_line.md @@ -0,0 +1,33 @@ +# SNS Line Plot +Generates a line plot from a pandas DataFrame using Seaborn. + +For plotting multiple lines, enter a comma-separated column labels in y_column_name. +If you want to display a legend for each line, enter a comma-separated legend label +in legend label. + +![SNS Line Plot](../images/sns_line.png) + +The `style` parameter can be chosen from: +* darkgrid (default) – Grid with a dark background. +* whitegrid – Grid with a white background. +* dark – Dark background, no grid. +* white – White background, no grid. +* ticks – White background with tick marks. + +The `palette` parameter can be chosen from: +* deep (default) – High contrast, good for multiple categories. +* muted – Softer colors, easier on the eyes. +* pastel – Light colors, suitable for minimalistic styles. +* bright – Vibrant and highly saturated colors. +* dark – Darker, high-contrast colors. +* colorblind – Optimized for color vision deficiencies. + +## Output +| Data type | +|---| +| Image | + +
+Category: Plot + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/sns_scatter.md b/docs/reference/sns_scatter.md new file mode 100644 index 0000000..24b81fd --- /dev/null +++ b/docs/reference/sns_scatter.md @@ -0,0 +1,14 @@ +# SNS Scatter Plot +Generates a scatter plot from a pandas DataFrame using Seaborn. + +For style and palette parameters, refer to [SNS Line Plot documentation](sns_line.md). + +## Output +| Data type | +|---| +| Image | + +
+Category: Plot + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/pandas_wrapper/utils.py b/modules/pandas_wrapper/utils.py index 9cac0c6..1113be3 100644 --- a/modules/pandas_wrapper/utils.py +++ b/modules/pandas_wrapper/utils.py @@ -27,6 +27,21 @@ def flatten_list_tuple_unique(lst: Union[List[Any], Tuple[Any, ...]]) -> Set[Any recurse(lst) return result +def comma_separated_labels_string_to_list(s:str) -> List[str]: + """ + Converts a comma-separated labels string into a list of string labels. + + Args: + s (str): A comma-separated labels. + + Returns: + List[str]: A list of valid labels. + """ + if s is None or s == "": + return [] + label_list = [e.strip() for e in s.split(",")] + return label_list + def column_labels_string_to_list(dataframe:pd.DataFrame, s:str, no_check:bool=False) -> List[Union[str, int]]: """ diff --git a/modules/seaborn_wrapper/common.py b/modules/seaborn_wrapper/common.py new file mode 100644 index 0000000..7212d2c --- /dev/null +++ b/modules/seaborn_wrapper/common.py @@ -0,0 +1,21 @@ +def common_input_types(): + """ + Defines the input types for a plot function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dataframe": ("DATAFRAME", {}), + "x_column_name": ("STRING", {"default": ""}), + "y_column_name": ("STRING", {"default": ""}), + "title": ("STRING", {"default": ""}), + "x_axis_label": ("STRING", {"default": ""}), + "y_axis_label": ("STRING", {"default": ""}), + "legend_label": ("STRING", {"default": ""}), + "x_tick_as_int": ("BOOLEAN", {"default": False}), # To show 2002 instead of 2002.5 on x-axis + "style": ("STRING", {"default": ""}), + "palette": ("STRING", {"default": ""}) + } + } diff --git a/modules/seaborn_wrapper/sns_bar.py b/modules/seaborn_wrapper/sns_bar.py new file mode 100644 index 0000000..7997192 --- /dev/null +++ b/modules/seaborn_wrapper/sns_bar.py @@ -0,0 +1,80 @@ +from typing import Any, Dict +from matplotlib import pyplot as plt +import pandas as pd +import seaborn as sns +from .common import common_input_types +from .util import plot_post_steps +from .util import set_up_sns_style_palette +from .util import process_y_columns_and_labels + + +class SNSBar: + """ + SNS Bar Chart: + Generates a bar chart from a Pandas DataFrame using Seaborn. + + For style and palette parameters, refer to [SNS Line Plot documentation](sns_line.md). + + category: Plot + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `bar_chart` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return common_input_types() + + RETURN_TYPES: tuple = ("IMAGE",) + FUNCTION: str = "bar_chart" + CATEGORY: str = "Data Analysis" + + def bar_chart(self, + dataframe: pd.DataFrame, + x_column_name: str, + y_column_name: str, + title: str, + x_axis_label: str, + y_axis_label: str, + legend_label: str, + x_tick_as_int: bool, + style: str=None, + palette: str=None) -> tuple: + """ + Generates a bar chart from a pandas DataFrame. + + Args: + dataframe (DataFrame): The DataFrame. + x_column_name (str): The column name for the x-axis. + y_column_name (str): The column name for the y-axis. + title (str): The title of the plot. + x_axis_label (str): The label for the x-axis data. + y_axis_label (str): The label for the y-axis data. + x_tick_as_int (bool): If set, use integers for x ticks. For example, + if ticks show floating point numbers for years, e.g. 2002.5, + setting this flag will show integer values instead. + style (str): Style of the plot + palette (str): Palette of the plot + Returns: + tuple: A tuple containing the image tensor representation of the plot. + """ + df = dataframe + set_up_sns_style_palette(style, palette) + y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) + + # Create the plot + fig, ax = plt.subplots() + for i in range(len(y_columns)): + sns.barplot(x=df[x_column_name], y=df[y_columns[i]], ax=ax, label=labels[i]) + + return plot_post_steps( + fig, + ax, + plt, + title, + x_axis_label, + y_axis_label, + x_tick_as_int) diff --git a/modules/seaborn_wrapper/sns_line.py b/modules/seaborn_wrapper/sns_line.py new file mode 100644 index 0000000..d4c85fa --- /dev/null +++ b/modules/seaborn_wrapper/sns_line.py @@ -0,0 +1,99 @@ +from typing import Any, Dict +from matplotlib import pyplot as plt +import seaborn as sns +import pandas as pd +from .common import common_input_types +from .util import plot_post_steps +from .util import set_up_sns_style_palette +from .util import process_y_columns_and_labels + + +class SNSLine: + """ + SNS Line Plot: + Generates a line plot from a pandas DataFrame using Seaborn. + + For plotting multiple lines, enter a comma-separated column labels in y_column_name. + If you want to display a legend for each line, enter a comma-separated legend label + in legend label. + + ![SNS Line Plot](../images/sns_line.png) + + The `style` parameter can be chosen from: + * darkgrid (default) – Grid with a dark background. + * whitegrid – Grid with a white background. + * dark – Dark background, no grid. + * white – White background, no grid. + * ticks – White background with tick marks. + + The `palette` parameter can be chosen from: + * deep (default) – High contrast, good for multiple categories. + * muted – Softer colors, easier on the eyes. + * pastel – Light colors, suitable for minimalistic styles. + * bright – Vibrant and highly saturated colors. + * dark – Darker, high-contrast colors. + * colorblind – Optimized for color vision deficiencies. + + category: Plot + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `line_plot` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return common_input_types() + + RETURN_TYPES: tuple = ("IMAGE",) + FUNCTION: str = "line_plot" + CATEGORY: str = "Data Analysis" + + def line_plot(self, + dataframe: pd.DataFrame, + x_column_name: str, + y_column_name: str, + title: str, + x_axis_label: str, + y_axis_label: str, + legend_label: str, + x_tick_as_int: bool, + style: str=None, + palette: str=None) -> tuple: + """ + Generates a line plot from a pandas DataFrame. + + Args: + dataframe (DataFrame): The DataFrame. + x_column_name (str): The column name for the x-axis. + y_column_name (str): The column name for the y-axis. + title (str): The title of the plot. + x_axis_label (str): The label for the x-axis data. + y_axis_label (str): The label for the y-axis data. + legend_label (str): The legend label of the plot. + x_tick_as_int (bool): If set, use integers for x ticks. For example, + if ticks show floating point numbers for years, e.g. 2002.5, + setting this flag will show integer values instead. + style (str): Style of the plot + palette (str): Palette of the plot + Returns: + tuple: A tuple containing the image tensor representation of the plot. + """ + df = dataframe + set_up_sns_style_palette(style, palette) + y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) + + fig, ax = plt.subplots() + for i in range(len(y_columns)): + sns.lineplot(x=df[x_column_name], y=df[y_columns[i]], ax=ax, label=labels[i]) + + return plot_post_steps( + fig, + ax, + plt, + title, + x_axis_label, + y_axis_label, + x_tick_as_int) diff --git a/modules/seaborn_wrapper/sns_scatter.py b/modules/seaborn_wrapper/sns_scatter.py new file mode 100644 index 0000000..b069ec3 --- /dev/null +++ b/modules/seaborn_wrapper/sns_scatter.py @@ -0,0 +1,80 @@ +from typing import Any, Dict +from matplotlib import pyplot as plt +import seaborn as sns +import pandas as pd +from .common import common_input_types +from .util import plot_post_steps +from .util import set_up_sns_style_palette +from .util import process_y_columns_and_labels + + +class SNSScatter: + """ + SNS Scatter Plot: + Generates a scatter plot from a pandas DataFrame using Seaborn. + + For style and palette parameters, refer to [SNS Line Plot documentation](sns_line.md). + + category: Plot + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `scatter_plot` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return common_input_types() + + RETURN_TYPES: tuple = ("IMAGE",) + FUNCTION: str = "scatter_plot" + CATEGORY: str = "Data Analysis" + + def scatter_plot(self, + dataframe: pd.DataFrame, + x_column_name: str, + y_column_name: str, + title: str, + x_axis_label: str, + y_axis_label: str, + legend_label: str, + x_tick_as_int: bool, + style: str=None, + palette: str=None) -> tuple: + """ + Generates a scatter plot from a pandas DataFrame. + + Args: + dataframe (DataFrame): The DataFrame. + x_column_name (str): The column name for the x-axis. + y_column_name (str): The column name for the y-axis. + title (str): The title of the plot. + x_axis_label (str): The label for the x-axis data. + y_axis_label (str): The label for the y-axis data. + x_tick_as_int (bool): If set, use integers for x ticks. For example, + if ticks show floating point numbers for years, e.g. 2002.5, + setting this flag will show integer values instead. + style (str): Style of the plot + palette (str): Palette of the plot + Returns: + tuple: A tuple containing the image tensor representation of the plot. + """ + df = dataframe + set_up_sns_style_palette(style, palette) + y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) + + # Create the plot + fig, ax = plt.subplots() + for i in range(len(y_columns)): + sns.scatterplot(x=df[x_column_name], y=df[y_columns[i]], ax=ax, label=labels[i]) + + return plot_post_steps( + fig, + ax, + plt, + title, + x_axis_label, + y_axis_label, + x_tick_as_int) diff --git a/modules/seaborn_wrapper/util.py b/modules/seaborn_wrapper/util.py new file mode 100644 index 0000000..54396cd --- /dev/null +++ b/modules/seaborn_wrapper/util.py @@ -0,0 +1,106 @@ +import io +from typing import List, Tuple +import numpy as np +import matplotlib.figure +import matplotlib.axes +from matplotlib import pyplot as plt +import matplotlib.ticker as mticker +import seaborn as sns +from PIL import Image +import torch +import pandas as pd +import seaborn as sns +from typing import Optional +from ..pandas_wrapper.utils import column_labels_string_to_list +from ..pandas_wrapper.utils import comma_separated_labels_string_to_list + + +def process_y_columns_and_labels(df:pd.DataFrame, y_column_name: str, legend_label: str) -> Tuple[List[str], List[str]]: + """ + Generates a y column list and legend label list from the comma-separated y column names and legend label string. + + Args: + df (pd.DataFrame): The input dataframe containing the data. + y_column_name (str): A comma-separated string of column names to be used as y-values. + legend_label (str): A comma-separated string of labels corresponding to the y columns. + + Returns: + List[str]: A list of processed y column names. + List[str]: A list of processed legend labels. + """ + y_columns = column_labels_string_to_list(df, y_column_name) + if legend_label: + labels = comma_separated_labels_string_to_list(legend_label) + if len(labels) < len(y_columns): + labels = labels + [""] * (len(y_columns) - len(labels)) + else: + labels = [""] * len(y_columns) + + return y_columns, labels + + +def set_up_sns_style_palette(style: Optional[str], palette: Optional[str]) -> None: + """ + Sets the Seaborn theme with the given style and palette. + + Args: + style (Optional[str]): The style to apply to the Seaborn theme (e.g., 'darkgrid', 'whitegrid'). + palette (Optional[str]): The color palette to apply (e.g., 'deep', 'muted'). + + Returns: + None + """ + if style and palette: + sns.set_theme(style=style, palette=palette) + elif style: + sns.set_theme(style=style) + elif palette: + sns.set_theme(palette=palette) + else: + sns.set_theme() + + +def plot_post_steps( + fig: matplotlib.figure.Figure, + ax: matplotlib.axes.Axes, + plt: plt, + title: str, + x_axis_label: str, + y_axis_label: str, + x_tick_as_int: bool +) -> tuple: + r"""Plots a post-processed step plot using Matplotlib. + + Args: + fig (matplotlib.figure.Figure): The figure object for the plot. + ax (matplotlib.axes.Axes): The axes object for the plot. + plt (module): The Matplotlib module. + title (str): The title of the plot. + x_axis_label (str): The label for the x-axis. + y_axis_label (str): The label for the y-axis. + x_tick_as_int (bool): If ``True``, use integers for x-axis ticks. + For example, if ticks display floating point numbers (e.g., 2002.5), + setting this flag forces integer values instead. + + Returns: + tuple: A tuple containing the image tensor representation of the plot. + """ + if x_tick_as_int: + ax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True)) + + # Set title and labels + ax.set_title(title) + ax.set_xlabel(x_axis_label) + ax.set_ylabel(y_axis_label) + + # Save the plot to a buffer + buf = io.BytesIO() + fig.savefig(buf, format='PNG') + buf.seek(0) + plt.close(fig) # Close the figure to free memory + + # Load image and convert to tensor + image = Image.open(buf).convert("RGB") + image_tensor = torch.from_numpy(np.array(image)).float() / 255.0 # Normalize to [0,1] + image_tensor = image_tensor.unsqueeze(0) + return (image_tensor,) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 810eb98..a300e76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "data-analysis" description = "Data analysis custom modules for ComfyUI - Use Pandas & Matplotlib within ComfyUI" -version = "1.1.25" +version = "1.2.0" license = {file = "LICENSE"} [project.urls] diff --git a/requirements.txt b/requirements.txt index 6382173..2dd4374 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ matplotlib>=3.10.0 pandas>=2.2.3 - +seaborn>=0.13.2 +scipy>=1.14.1 \ No newline at end of file diff --git a/tests/pandas_wrapper/utils_comma_separated_labels_string_to_list_test.py b/tests/pandas_wrapper/utils_comma_separated_labels_string_to_list_test.py new file mode 100644 index 0000000..67e51eb --- /dev/null +++ b/tests/pandas_wrapper/utils_comma_separated_labels_string_to_list_test.py @@ -0,0 +1,38 @@ +import os +import sys +import unittest +from typing import List + +PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pandas_wrapper.utils import comma_separated_labels_string_to_list + +class TestCommaSeparatedLabelsStringToList(unittest.TestCase): + """ + Unit tests for `comma_separated_labels_string_to_list` function. + """ + + def setUp(self) -> None: + """ + Sets up the test environment before each test case. + """ + pass + + def test_valid_column_strings(self) -> None: + """ + Tests `comma_separated_labels_string_to_list` with various valid input strings. + """ + self.assertEqual(comma_separated_labels_string_to_list("label1,label2,label3"), ["label1", "label2", "label3"]) + self.assertEqual(comma_separated_labels_string_to_list(" label1 , label2 ,label3 "), ["label1", "label2", "label3"]) + self.assertEqual(comma_separated_labels_string_to_list("a,b,c,d"), ["a", "b", "c", "d"]) + self.assertEqual(comma_separated_labels_string_to_list("one"), ["one"]) + self.assertEqual(comma_separated_labels_string_to_list(""), []) + self.assertEqual(comma_separated_labels_string_to_list(None), []) + self.assertEqual(comma_separated_labels_string_to_list(" , , , "), ["", "", "", ""]) + +if __name__ == "__main__": + unittest.main() + +