Skip to content

Word Clouds¤

Static Word Clouds¤

WordCloud pydantic-model ¤

Bases: BaseModel

A Pydantic model for WordCloud options.

Config:

  • arbitrary_types_allowed: True
  • json_schema_extra: DocJSONSchema.model_json_schema()

Fields:

  • data (single_doc_types | multi_doc_types | DataFrame)
  • docs (Optional[int | str | list[int] | list[str]])
  • limit (Optional[int])
  • title (Optional[str])
  • height (int)
  • width (int)
  • opts (dict[str, Any])
  • figure_opts (dict[str, Any])
  • round (Optional[int])
  • counts (dict[str, int])
  • cloud (WordCloud | None)
  • fig (Optional[Figure])
Source code in lexos/visualization/cloud.py
class WordCloud(BaseModel):
    """A Pydantic model for WordCloud options."""

    data: single_doc_types | multi_doc_types | pd.DataFrame = Field(
        ...,
        description="The data to generate the word cloud from. Accepts data from a string, list of lists or tuples, a dict with terms as keys and counts/frequencies as values, or a dataframe.",
    )
    docs: Optional[int | str | list[int] | list[str]] = Field(
        None, description="A list of documents to be selected from the DTM."
    )
    limit: Optional[int] = Field(
        None, description="The maximum number of terms to plot."
    )
    title: Optional[str] = Field(None, description="The title of the plot.")
    height: int = Field(
        200, gt=50, description="The height of the word cloud in pixels."
    )
    width: int = Field(200, gt=50, description="The width of the word cloud in pixels.")
    opts: dict[str, Any] = Field(
        default_factory=lambda: {
            "background_color": "white",
            "max_words": 2000,
            "contour_width": 0,
            "contour_color": "steelblue",
        },
        description="The WordCloud() options.",
    )
    figure_opts: dict[str, Any] = Field(
        default_factory=dict,
        description="A dict of matplotlib figure options.",
    )
    round: Optional[int] = Field(
        0,
        description="An integer to apply a mask that rounds the word cloud. It is best to use 100 or higher for a circular mask, but it will depend on the height and width of the word cloud.",
    )
    counts: dict[str, int] = Field(None, description="A dictionary of term counts.")
    cloud: PythonWordCloud | None = Field(
        None, description="The generated WordCloud object."
    )
    fig: Optional[plt.Figure] = Field(
        None, description="The matplotlib figure object for the word cloud."
    )

    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        json_schema_extra=DocJSONSchema.model_json_schema(),
    )

    def __init__(self, **data: Any) -> None:
        """Initialize the WordCloud model."""
        super().__init__(**data)

        # Set the figure dimensions
        self.opts["height"] = self.height
        self.opts["width"] = self.width

        # Set the mask, if using
        if self.round > 0:
            x, y = np.ogrid[:300, :300]
            mask = (x - 150) ** 2 + (y - 150) ** 2 > self.round**2
            mask = 255 * mask.astype(int)
            self.opts["mask"] = mask

        # Process the data into a consistent format
        self.counts = processors.process_data(self.data, self.docs, self.limit)

        # Generate the word cloud
        self.cloud = PythonWordCloud(**self.opts).generate_from_frequencies(self.counts)

    @validate_call
    def save(self, path: Path | str, **kwargs: Any) -> None:
        """Save the WordCloud to a file.

        Args:
            path (Path | str): The file path to save the WordCloud image.
            **kwargs (Any): Additional keyword arguments for `plt.savefig`.
        """
        if self.cloud is None:
            raise LexosException("No WordCloud object to save.")
        self.fig = plt.figure(**self.figure_opts)
        ax = self.fig.add_subplot(111)
        if self.title:
            self.fig.suptitle(self.title)
        ax.axis("off")
        ax.imshow(self.cloud.to_array(), interpolation="bilinear")
        self.fig.savefig(path, **kwargs)
        plt.close(self.fig)

    def show(self) -> None:
        """Show the figure if it is hidden.

        This is a helper method. It will generally display in a
        Jupyter notebook.
        """
        self.fig = plt.figure(**self.figure_opts)
        if self.title:
            self.fig.suptitle(self.title)
        plt.axis("off")
        plt.imshow(self.cloud.to_array(), interpolation="bilinear")

data: single_doc_types | multi_doc_types | pd.DataFrame pydantic-field ¤

The data to generate the word cloud from. Accepts data from a string, list of lists or tuples, a dict with terms as keys and counts/frequencies as values, or a dataframe.

docs: Optional[int | str | list[int] | list[str]] = None pydantic-field ¤

A list of documents to be selected from the DTM.

fig: Optional[plt.Figure] = None pydantic-field ¤

The matplotlib figure object for the word cloud.

figure_opts: dict[str, Any] pydantic-field ¤

A dict of matplotlib figure options.

height: int = 200 pydantic-field ¤

The height of the word cloud in pixels.

limit: Optional[int] = None pydantic-field ¤

The maximum number of terms to plot.

opts: dict[str, Any] pydantic-field ¤

The WordCloud() options.

round: Optional[int] = 0 pydantic-field ¤

An integer to apply a mask that rounds the word cloud. It is best to use 100 or higher for a circular mask, but it will depend on the height and width of the word cloud.

title: Optional[str] = None pydantic-field ¤

The title of the plot.

width: int = 200 pydantic-field ¤

The width of the word cloud in pixels.

__init__(**data: Any) -> None ¤

Initialize the WordCloud model.

Source code in lexos/visualization/cloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the WordCloud model."""
    super().__init__(**data)

    # Set the figure dimensions
    self.opts["height"] = self.height
    self.opts["width"] = self.width

    # Set the mask, if using
    if self.round > 0:
        x, y = np.ogrid[:300, :300]
        mask = (x - 150) ** 2 + (y - 150) ** 2 > self.round**2
        mask = 255 * mask.astype(int)
        self.opts["mask"] = mask

    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)

    # Generate the word cloud
    self.cloud = PythonWordCloud(**self.opts).generate_from_frequencies(self.counts)

save(path: Path | str, **kwargs: Any) -> None ¤

Save the WordCloud to a file.

Parameters:

Name Type Description Default
path Path | str

The file path to save the WordCloud image.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/cloud.py
@validate_call
def save(self, path: Path | str, **kwargs: Any) -> None:
    """Save the WordCloud to a file.

    Args:
        path (Path | str): The file path to save the WordCloud image.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if self.cloud is None:
        raise LexosException("No WordCloud object to save.")
    self.fig = plt.figure(**self.figure_opts)
    ax = self.fig.add_subplot(111)
    if self.title:
        self.fig.suptitle(self.title)
    ax.axis("off")
    ax.imshow(self.cloud.to_array(), interpolation="bilinear")
    self.fig.savefig(path, **kwargs)
    plt.close(self.fig)

show() -> None ¤

Show the figure if it is hidden.

This is a helper method. It will generally display in a Jupyter notebook.

Source code in lexos/visualization/cloud.py
def show(self) -> None:
    """Show the figure if it is hidden.

    This is a helper method. It will generally display in a
    Jupyter notebook.
    """
    self.fig = plt.figure(**self.figure_opts)
    if self.title:
        self.fig.suptitle(self.title)
    plt.axis("off")
    plt.imshow(self.cloud.to_array(), interpolation="bilinear")

data: single_doc_types | multi_doc_types | pd.DataFrame pydantic-field ¤

The data to generate the word cloud from. Accepts data from a string, list of lists or tuples, a dict with terms as keys and counts/frequencies as values, or a dataframe.

docs: Optional[int | str | list[int] | list[str]] = None pydantic-field ¤

A list of documents to be selected from the DTM.

limit: Optional[int] = None pydantic-field ¤

The maximum number of terms to plot.

title: Optional[str] = None pydantic-field ¤

The title of the plot.

height: int = 200 pydantic-field ¤

The height of the word cloud in pixels.

width: int = 200 pydantic-field ¤

The width of the word cloud in pixels.

opts: dict[str, Any] pydantic-field ¤

The WordCloud() options.

figure_opts: dict[str, Any] pydantic-field ¤

A dict of matplotlib figure options.

round: Optional[int] = 0 pydantic-field ¤

An integer to apply a mask that rounds the word cloud. It is best to use 100 or higher for a circular mask, but it will depend on the height and width of the word cloud.

counts: dict[str, int] pydantic-field ¤

cloud: PythonWordCloud | None pydantic-field ¤

fig: Optional[plt.Figure] = None pydantic-field ¤

The matplotlib figure object for the word cloud.

model_config = ConfigDict(arbitrary_types_allowed=True, json_schema_extra=(DocJSONSchema.model_json_schema())) class-attribute instance-attribute ¤

__init__(**data: Any) -> None ¤

Initialize the WordCloud model.

Source code in lexos/visualization/cloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the WordCloud model."""
    super().__init__(**data)

    # Set the figure dimensions
    self.opts["height"] = self.height
    self.opts["width"] = self.width

    # Set the mask, if using
    if self.round > 0:
        x, y = np.ogrid[:300, :300]
        mask = (x - 150) ** 2 + (y - 150) ** 2 > self.round**2
        mask = 255 * mask.astype(int)
        self.opts["mask"] = mask

    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)

    # Generate the word cloud
    self.cloud = PythonWordCloud(**self.opts).generate_from_frequencies(self.counts)

save(path: Path | str, **kwargs: Any) -> None ¤

Save the WordCloud to a file.

Parameters:

Name Type Description Default
path Path | str

The file path to save the WordCloud image.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/cloud.py
@validate_call
def save(self, path: Path | str, **kwargs: Any) -> None:
    """Save the WordCloud to a file.

    Args:
        path (Path | str): The file path to save the WordCloud image.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if self.cloud is None:
        raise LexosException("No WordCloud object to save.")
    self.fig = plt.figure(**self.figure_opts)
    ax = self.fig.add_subplot(111)
    if self.title:
        self.fig.suptitle(self.title)
    ax.axis("off")
    ax.imshow(self.cloud.to_array(), interpolation="bilinear")
    self.fig.savefig(path, **kwargs)
    plt.close(self.fig)

show() -> None ¤

Show the figure if it is hidden.

This is a helper method. It will generally display in a Jupyter notebook.

Source code in lexos/visualization/cloud.py
def show(self) -> None:
    """Show the figure if it is hidden.

    This is a helper method. It will generally display in a
    Jupyter notebook.
    """
    self.fig = plt.figure(**self.figure_opts)
    if self.title:
        self.fig.suptitle(self.title)
    plt.axis("off")
    plt.imshow(self.cloud.to_array(), interpolation="bilinear")

MultiCloud pydantic-model ¤

Bases: BaseModel

A Pydantic model for creating multiple WordClouds arranged in a grid using the topic_clouds approach.

Config:

  • arbitrary_types_allowed: True
  • json_schema_extra: DocJSONSchema.model_json_schema()

Fields:

  • data (list[str] | list[list[str]] | list[Doc] | list[Span] | DTM | DataFrame)
  • docs (Optional[int | str | list[int] | list[str]])
  • limit (Optional[int])
  • figsize (tuple[int, int])
  • layout (Optional[str | tuple[int, int]])
  • opts (dict[str, Any])
  • round (Optional[int])
  • title (Optional[str])
  • labels (Optional[list[str]])
  • doc_data (Optional[list[dict[str, int | float]]])
  • fig (Optional[Figure])
  • wordcloud (Optional[WordCloud])
Source code in lexos/visualization/cloud.py
class MultiCloud(BaseModel):
    """A Pydantic model for creating multiple WordClouds arranged in a grid using the topic_clouds approach."""

    data: list[str] | list[list[str]] | list[Doc] | list[Span] | DTM | pd.DataFrame = (
        Field(
            ...,
            description="The data to generate word clouds from. Accepts list of documents, DTM, or DataFrame.",
        )
    )
    docs: Optional[int | str | list[int] | list[str]] = Field(
        None, description="A list of documents to be selected from the DTM/DataFrame."
    )
    limit: Optional[int] = Field(
        None, description="The maximum number of terms to plot per cloud."
    )
    figsize: tuple[int, int] = Field(
        (10, 10), description="The size of the overall figure."
    )
    layout: Optional[str | tuple[int, int]] = Field(
        "auto",
        description="The number of rows and columns in the figure. Default is 'auto'.",
    )
    opts: dict[str, Any] = Field(
        default_factory=lambda: {
            "background_color": "white",
            "max_words": 2000,
            "contour_width": 0,
            "contour_color": "steelblue",
        },
        description="The WordCloud() options applied to each word cloud.",
    )
    round: Optional[int] = Field(
        0,
        description="An integer to apply a mask that rounds each word cloud. It is best to use 100 or higher for a circular mask.",
    )
    title: Optional[str] = Field(None, description="Overall title for the figure.")
    labels: Optional[list[str]] = Field(
        None, description="Labels for each subplot/word cloud."
    )
    doc_data: Optional[list[dict[str, int | float]]] = Field(
        None, description="Processed document data for each word cloud."
    )
    fig: Optional[plt.Figure] = Field(
        None, description="The matplotlib figure object for the multi-cloud plot."
    )
    wordcloud: Optional[PythonWordCloud] = Field(
        None, description="The WordCloud object used for generating clouds."
    )

    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        json_schema_extra=DocJSONSchema.model_json_schema(),
    )

    def __init__(self, **data: Any) -> None:
        """Initialize the MultiCloud model."""
        super().__init__(**data)

        # Process different data types to get individual document data
        self.doc_data = self._process_data()

        # Setup the WordCloud object
        self.wordcloud = self._setup_wordcloud()

        # Render the figure
        self._render()

    def _process_data(self) -> list[dict[str, int | float]]:
        """Process the input data into individual document dictionaries."""
        if isinstance(self.data, DTM):
            return self._process_dtm()
        if isinstance(self.data, pd.DataFrame):
            return self._process_dataframe()
        if isinstance(self.data, list):
            return self._process_list()

        raise LexosException("Unsupported data type for MultiCloud.")

    def _process_dtm(self) -> list[dict[str, int | float]]:
        """Process a DTM into document frequency dictionaries."""
        if self.data.doc_term_matrix is None or self.data.doc_term_matrix.shape[0] == 0:
            raise LexosException("Empty DTM provided.")

        selected_docs = self._normalize_selected_docs(
            self.docs,
            range(self.data.doc_term_matrix.shape[0]),
            lambda label: self.data.labels.index(label),
        )

        return [self._process_dtm_row(doc_idx) for doc_idx in selected_docs]

    def _process_dataframe(self) -> list[dict[str, int | float]]:
        """Process a DataFrame into document frequency dictionaries."""
        if self.data.empty:
            raise LexosException("Empty DataFrame provided.")

        selected_docs = self._normalize_selected_docs(
            self.docs,
            range(len(self.data)),
            lambda label: self.data.index.get_loc(label),
        )

        return [self._process_dataframe_row(doc_idx) for doc_idx in selected_docs]

    def _process_list(self) -> list[dict[str, int | float]]:
        """Process a list of documents into word frequencies."""
        if not self.data:
            raise LexosException("No valid data provided for MultiCloud.")

        return [processors.process_data(doc, None, self.limit) for doc in self.data]

    def _normalize_selected_docs(
        self,
        docs: Optional[int | str | list[int] | list[str]],
        default_docs: range,
        label_mapper: Any,
    ) -> list[int | str]:
        """Normalize selection values into a list of document indices."""
        if docs is None:
            docs = default_docs
        if isinstance(docs, (int, str)):
            docs = [docs]

        return [label_mapper(doc) if isinstance(doc, str) else doc for doc in docs]

    def _process_dtm_row(self, doc_idx: int | str) -> dict[str, int | float]:
        if isinstance(doc_idx, str):
            doc_idx = self.data.labels.index(doc_idx)

        doc_row = self.data.doc_term_matrix[doc_idx]
        if hasattr(doc_row, "toarray"):
            doc_row = doc_row.toarray().flatten()

        return self._filter_positive_counts(
            {
                term: self._to_float(count)
                for term, count in zip(self.data.vectorizer.terms_list, doc_row)
            }
        )

    def _process_dataframe_row(self, doc_idx: int | str) -> dict[str, int | float]:
        if isinstance(doc_idx, str):
            doc_idx = self.data.index.get_loc(doc_idx)

        return self._filter_positive_counts(self.data.iloc[doc_idx].to_dict())

    def _filter_positive_counts(self, counts: dict[str, Any]) -> dict[str, int | float]:
        return {
            term: value
            for term, value in ((k, self._to_float(v)) for k, v in counts.items())
            if value > 0
        }

    def _to_float(self, value: Any) -> float:
        return float(value.item()) if hasattr(value, "item") else float(value)

    def _setup_wordcloud(self) -> PythonWordCloud:
        """Configure a single WordCloud object to be reused."""
        # Set the mask if using round
        if self.round > 0:
            x, y = np.ogrid[:300, :300]
            mask = (x - 150) ** 2 + (y - 150) ** 2 > self.round**2
            mask = 255 * mask.astype(int)
            self.opts["mask"] = mask

        # Set max_words if limit is specified
        if self.limit:
            self.opts["max_words"] = self.limit

        return PythonWordCloud(**self.opts)

    def _render(self) -> None:
        """Generate and display the multi-cloud figure."""
        # Create a local figure without mutating global matplotlib style
        self.fig = plt.figure(figsize=self.figsize)

        # Calculate layout
        n = len(self.doc_data)
        if self.layout == "auto":
            columns = math.floor(math.sqrt(n))
            rows = math.ceil(n / columns)
        elif isinstance(self.layout, tuple):
            rows, columns = self.layout
        else:
            raise LexosException("Invalid layout specification.")

        # Add overall title
        if self.title:
            self.fig.suptitle(self.title, fontsize=16)

        # Generate the word clouds
        for i, doc_counts in enumerate(self.doc_data):
            self.wordcloud.generate_from_frequencies(doc_counts)
            ax = self.fig.add_subplot(rows, columns, i + 1)
            ax.imshow(self.wordcloud.to_array(), interpolation="bilinear")
            ax.axis("off")

            # Add label if provided
            if self.labels and i < len(self.labels):
                ax.set_title(self.labels[i])
            else:
                ax.set_title(f"Doc {i}")

        # Get the figure and close to prevent automatic display
        self.fig = plt.gcf()
        plt.close()

    @validate_call
    def save(self, path: Path | str, **kwargs: Any) -> None:
        """Save the MultiCloud figure to a file.

        Args:
            path (Path | str): The file path to save the MultiCloud image.
            **kwargs (Any): Additional keyword arguments for `plt.savefig`.
        """
        if self.fig is None:
            raise LexosException("No figure to save.")
        self.fig.savefig(path, **kwargs)

    def show(self) -> None:
        """Display the multi-cloud figure."""
        if self.fig is None:
            raise LexosException("No figure to show.")
        # Use IPython display for Jupyter notebooks
        try:
            from IPython.display import display

            display(self.fig)
        except ImportError:
            # Fallback for non-Jupyter environments
            plt.figure(self.fig.number)
            plt.show()

data: list[str] | list[list[str]] | list[Doc] | list[Span] | DTM | pd.DataFrame pydantic-field ¤

The data to generate word clouds from. Accepts list of documents, DTM, or DataFrame.

docs: Optional[int | str | list[int] | list[str]] = None pydantic-field ¤

A list of documents to be selected from the DTM/DataFrame.

fig: Optional[plt.Figure] = None pydantic-field ¤

The matplotlib figure object for the multi-cloud plot.

figsize: tuple[int, int] = (10, 10) pydantic-field ¤

The size of the overall figure.

labels: Optional[list[str]] = None pydantic-field ¤

Labels for each subplot/word cloud.

layout: Optional[str | tuple[int, int]] = 'auto' pydantic-field ¤

The number of rows and columns in the figure. Default is 'auto'.

limit: Optional[int] = None pydantic-field ¤

The maximum number of terms to plot per cloud.

opts: dict[str, Any] pydantic-field ¤

The WordCloud() options applied to each word cloud.

round: Optional[int] = 0 pydantic-field ¤

An integer to apply a mask that rounds each word cloud. It is best to use 100 or higher for a circular mask.

title: Optional[str] = None pydantic-field ¤

Overall title for the figure.

__init__(**data: Any) -> None ¤

Initialize the MultiCloud model.

Source code in lexos/visualization/cloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the MultiCloud model."""
    super().__init__(**data)

    # Process different data types to get individual document data
    self.doc_data = self._process_data()

    # Setup the WordCloud object
    self.wordcloud = self._setup_wordcloud()

    # Render the figure
    self._render()

save(path: Path | str, **kwargs: Any) -> None ¤

Save the MultiCloud figure to a file.

Parameters:

Name Type Description Default
path Path | str

The file path to save the MultiCloud image.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/cloud.py
@validate_call
def save(self, path: Path | str, **kwargs: Any) -> None:
    """Save the MultiCloud figure to a file.

    Args:
        path (Path | str): The file path to save the MultiCloud image.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if self.fig is None:
        raise LexosException("No figure to save.")
    self.fig.savefig(path, **kwargs)

show() -> None ¤

Display the multi-cloud figure.

Source code in lexos/visualization/cloud.py
def show(self) -> None:
    """Display the multi-cloud figure."""
    if self.fig is None:
        raise LexosException("No figure to show.")
    # Use IPython display for Jupyter notebooks
    try:
        from IPython.display import display

        display(self.fig)
    except ImportError:
        # Fallback for non-Jupyter environments
        plt.figure(self.fig.number)
        plt.show()

data: list[str] | list[list[str]] | list[Doc] | list[Span] | DTM | pd.DataFrame pydantic-field ¤

The data to generate word clouds from. Accepts list of documents, DTM, or DataFrame.

docs: Optional[int | str | list[int] | list[str]] = None pydantic-field ¤

A list of documents to be selected from the DTM/DataFrame.

limit: Optional[int] = None pydantic-field ¤

The maximum number of terms to plot per cloud.

figsize: tuple[int, int] = (10, 10) pydantic-field ¤

The size of the overall figure.

layout: Optional[str | tuple[int, int]] = 'auto' pydantic-field ¤

The number of rows and columns in the figure. Default is 'auto'.

opts: dict[str, Any] pydantic-field ¤

The WordCloud() options applied to each word cloud.

round: Optional[int] = 0 pydantic-field ¤

An integer to apply a mask that rounds each word cloud. It is best to use 100 or higher for a circular mask.

title: Optional[str] = None pydantic-field ¤

Overall title for the figure.

labels: Optional[list[str]] = None pydantic-field ¤

Labels for each subplot/word cloud.

doc_data: Optional[list[dict[str, int | float]]] pydantic-field ¤

fig: Optional[plt.Figure] = None pydantic-field ¤

The matplotlib figure object for the multi-cloud plot.

wordcloud: Optional[PythonWordCloud] pydantic-field ¤

model_config = ConfigDict(arbitrary_types_allowed=True, json_schema_extra=(DocJSONSchema.model_json_schema())) class-attribute instance-attribute ¤

__init__(**data: Any) -> None ¤

Initialize the MultiCloud model.

Source code in lexos/visualization/cloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the MultiCloud model."""
    super().__init__(**data)

    # Process different data types to get individual document data
    self.doc_data = self._process_data()

    # Setup the WordCloud object
    self.wordcloud = self._setup_wordcloud()

    # Render the figure
    self._render()

_process_data() -> list[dict[str, int | float]] ¤

Process the input data into individual document dictionaries.

Source code in lexos/visualization/cloud.py
def _process_data(self) -> list[dict[str, int | float]]:
    """Process the input data into individual document dictionaries."""
    if isinstance(self.data, DTM):
        return self._process_dtm()
    if isinstance(self.data, pd.DataFrame):
        return self._process_dataframe()
    if isinstance(self.data, list):
        return self._process_list()

    raise LexosException("Unsupported data type for MultiCloud.")

_process_dtm() -> list[dict[str, int | float]] ¤

Process a DTM into document frequency dictionaries.

Source code in lexos/visualization/cloud.py
def _process_dtm(self) -> list[dict[str, int | float]]:
    """Process a DTM into document frequency dictionaries."""
    if self.data.doc_term_matrix is None or self.data.doc_term_matrix.shape[0] == 0:
        raise LexosException("Empty DTM provided.")

    selected_docs = self._normalize_selected_docs(
        self.docs,
        range(self.data.doc_term_matrix.shape[0]),
        lambda label: self.data.labels.index(label),
    )

    return [self._process_dtm_row(doc_idx) for doc_idx in selected_docs]

_process_dataframe() -> list[dict[str, int | float]] ¤

Process a DataFrame into document frequency dictionaries.

Source code in lexos/visualization/cloud.py
def _process_dataframe(self) -> list[dict[str, int | float]]:
    """Process a DataFrame into document frequency dictionaries."""
    if self.data.empty:
        raise LexosException("Empty DataFrame provided.")

    selected_docs = self._normalize_selected_docs(
        self.docs,
        range(len(self.data)),
        lambda label: self.data.index.get_loc(label),
    )

    return [self._process_dataframe_row(doc_idx) for doc_idx in selected_docs]

_process_list() -> list[dict[str, int | float]] ¤

Process a list of documents into word frequencies.

Source code in lexos/visualization/cloud.py
def _process_list(self) -> list[dict[str, int | float]]:
    """Process a list of documents into word frequencies."""
    if not self.data:
        raise LexosException("No valid data provided for MultiCloud.")

    return [processors.process_data(doc, None, self.limit) for doc in self.data]

_normalize_selected_docs(docs: Optional[int | str | list[int] | list[str]], default_docs: range, label_mapper: Any) -> list[int | str] ¤

Normalize selection values into a list of document indices.

Source code in lexos/visualization/cloud.py
def _normalize_selected_docs(
    self,
    docs: Optional[int | str | list[int] | list[str]],
    default_docs: range,
    label_mapper: Any,
) -> list[int | str]:
    """Normalize selection values into a list of document indices."""
    if docs is None:
        docs = default_docs
    if isinstance(docs, (int, str)):
        docs = [docs]

    return [label_mapper(doc) if isinstance(doc, str) else doc for doc in docs]

_process_dtm_row(doc_idx: int | str) -> dict[str, int | float] ¤

Source code in lexos/visualization/cloud.py
def _process_dtm_row(self, doc_idx: int | str) -> dict[str, int | float]:
    if isinstance(doc_idx, str):
        doc_idx = self.data.labels.index(doc_idx)

    doc_row = self.data.doc_term_matrix[doc_idx]
    if hasattr(doc_row, "toarray"):
        doc_row = doc_row.toarray().flatten()

    return self._filter_positive_counts(
        {
            term: self._to_float(count)
            for term, count in zip(self.data.vectorizer.terms_list, doc_row)
        }
    )

_process_dataframe_row(doc_idx: int | str) -> dict[str, int | float] ¤

Source code in lexos/visualization/cloud.py
def _process_dataframe_row(self, doc_idx: int | str) -> dict[str, int | float]:
    if isinstance(doc_idx, str):
        doc_idx = self.data.index.get_loc(doc_idx)

    return self._filter_positive_counts(self.data.iloc[doc_idx].to_dict())

_filter_positive_counts(counts: dict[str, Any]) -> dict[str, int | float] ¤

Source code in lexos/visualization/cloud.py
def _filter_positive_counts(self, counts: dict[str, Any]) -> dict[str, int | float]:
    return {
        term: value
        for term, value in ((k, self._to_float(v)) for k, v in counts.items())
        if value > 0
    }

_to_float(value: Any) -> float ¤

Source code in lexos/visualization/cloud.py
def _to_float(self, value: Any) -> float:
    return float(value.item()) if hasattr(value, "item") else float(value)

_setup_wordcloud() -> PythonWordCloud ¤

Configure a single WordCloud object to be reused.

Source code in lexos/visualization/cloud.py
def _setup_wordcloud(self) -> PythonWordCloud:
    """Configure a single WordCloud object to be reused."""
    # Set the mask if using round
    if self.round > 0:
        x, y = np.ogrid[:300, :300]
        mask = (x - 150) ** 2 + (y - 150) ** 2 > self.round**2
        mask = 255 * mask.astype(int)
        self.opts["mask"] = mask

    # Set max_words if limit is specified
    if self.limit:
        self.opts["max_words"] = self.limit

    return PythonWordCloud(**self.opts)

_render() -> None ¤

Generate and display the multi-cloud figure.

Source code in lexos/visualization/cloud.py
def _render(self) -> None:
    """Generate and display the multi-cloud figure."""
    # Create a local figure without mutating global matplotlib style
    self.fig = plt.figure(figsize=self.figsize)

    # Calculate layout
    n = len(self.doc_data)
    if self.layout == "auto":
        columns = math.floor(math.sqrt(n))
        rows = math.ceil(n / columns)
    elif isinstance(self.layout, tuple):
        rows, columns = self.layout
    else:
        raise LexosException("Invalid layout specification.")

    # Add overall title
    if self.title:
        self.fig.suptitle(self.title, fontsize=16)

    # Generate the word clouds
    for i, doc_counts in enumerate(self.doc_data):
        self.wordcloud.generate_from_frequencies(doc_counts)
        ax = self.fig.add_subplot(rows, columns, i + 1)
        ax.imshow(self.wordcloud.to_array(), interpolation="bilinear")
        ax.axis("off")

        # Add label if provided
        if self.labels and i < len(self.labels):
            ax.set_title(self.labels[i])
        else:
            ax.set_title(f"Doc {i}")

    # Get the figure and close to prevent automatic display
    self.fig = plt.gcf()
    plt.close()

save(path: Path | str, **kwargs: Any) -> None ¤

Save the MultiCloud figure to a file.

Parameters:

Name Type Description Default
path Path | str

The file path to save the MultiCloud image.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/cloud.py
@validate_call
def save(self, path: Path | str, **kwargs: Any) -> None:
    """Save the MultiCloud figure to a file.

    Args:
        path (Path | str): The file path to save the MultiCloud image.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if self.fig is None:
        raise LexosException("No figure to save.")
    self.fig.savefig(path, **kwargs)

show() -> None ¤

Display the multi-cloud figure.

Source code in lexos/visualization/cloud.py
def show(self) -> None:
    """Display the multi-cloud figure."""
    if self.fig is None:
        raise LexosException("No figure to show.")
    # Use IPython display for Jupyter notebooks
    try:
        from IPython.display import display

        display(self.fig)
    except ImportError:
        # Fallback for non-Jupyter environments
        plt.figure(self.fig.number)
        plt.show()

D3 Word Clouds¤

D3WordCloud pydantic-model ¤

Bases: BaseModel

A Pydantic model for D3 WordCloud options.

Config:

  • arbitrary_types_allowed: True
  • json_schema_extra: DocJSONSchema.model_json_schema()

Fields:

Validators:

Source code in lexos/visualization/d3_wordcloud.py
class D3WordCloud(BaseModel):
    """A Pydantic model for D3 WordCloud options."""

    data: single_doc_types | multi_doc_types | pd.DataFrame = Field(
        ...,
        description="The data to generate the word cloud from. Accepts data from a string, list of lists or tuples, a dict with terms as keys and counts/frequencies as values, or a dataframe.",
    )
    docs: Optional[int | str | list[int] | list[str]] = Field(
        None, description="A list of documents to be selected from the DTM."
    )
    layout: dict[str, Any] = Field(default_factory=dict)
    limit: int = Field(100, description="The maximum number of terms in the cloud.")
    font: str = Field("Impact", description="The font to use for the word cloud.")
    spiral: str = Field(
        "archimedean",
        description="The spiral type to use for the word cloud, 'archimedean' or 'rectangular'.",
    )
    scale: str = Field(
        "log",
        description="The scale type to use for the word cloud, 'log', 'sqrt', or 'linear'.",
    )
    angle_count: int = Field(
        5, description="The number of angles to use for the word cloud."
    )
    angle_from: int = Field(-60, description="The starting angle for the word cloud.")
    angle_to: int = Field(60, description="The ending angle for the word cloud.")
    width: int = Field(600, gt=50, description="The width of the word cloud in pixels.")
    height: int = Field(
        600, gt=50, description="The height of the word cloud in pixels."
    )
    title: str = Field(
        "Word Cloud Visualization", description="The title of the word cloud."
    )
    background_color: str = Field(
        "white", description="The background color of the word cloud."
    )
    colorscale: str = Field(
        "d3.scale.category20b",
        description="The name of a categorical d3 scale to use for the word cloud. See https://d3js.org/d3-scale.",
    )
    auto_open: bool = Field(
        True, description="Whether to open the chart in a web browser automatically."
    )
    template: Path | str = Field(
        "d3_cloud_template-1.0.html",
        description="The template file for the word cloud.",
    )
    include_d3js: bool | str | None = Field(
        True,
        description="Whether to include the D3.js library. Can be 'cdn', 'directory', or a custom path. If False, the D3.js library will not be included. The cloud bundle is always included unless the setting is 'directory' or False.",
    )
    include_d3_cloud: bool | str = Field(
        True,
        description="Whether to include the D3 cloud library. Can be a custom path to a JavaScript file or True to use the default bundled version.",
    )
    counts: dict[str, int] = Field({}, description="A dictionary of word counts.")
    html: str = Field("", description="The HTML representation of the word cloud.")

    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        json_schema_extra=DocJSONSchema.model_json_schema(),
    )

    @field_validator("spiral")
    @classmethod
    def validate_spiral(cls, v):
        """Validate the spiral setting."""
        if v not in ["archimedean", "rectangular"]:
            raise ValueError('spiral must be "archimedean" or "rectangular"')
        return v

    @field_validator("scale")
    @classmethod
    def validate_scale(cls, v):
        """Validate the scale setting."""
        if v not in ["log", "sqrt", "linear"]:
            raise ValueError('scale must be "log", "sqrt", or "linear"')
        return v

    @model_validator(mode="after")
    def validate_angles(self):
        """Validate the angle settings."""
        if self.angle_from >= self.angle_to:
            raise ValueError("angle_from must be less than angle_to")
        return self

    def __init__(self, **data: Any) -> None:
        """Initialize with better error handling."""
        try:
            super().__init__(**data)
        except Exception as e:
            raise LexosException(f"Failed to initialize D3WordCloud: {e}") from e

        # Process the data into a consistent format
        self.counts = processors.process_data(self.data, self.docs, self.limit)
        self._render()
        self._include_d3()
        self._include_d3_cloud()

    def _render(self) -> None:
        """Render the word cloud as an HTML string."""
        template = Template(self._load_template())
        self.html = template.render(
            termCounts=json.dumps(self.counts),
            width=self.width,
            height=self.height,
            title=self.title,
            backgroundColor=self.background_color,
            colorscale=self.colorscale,
            font=self.font,
            spiral=self.spiral,
            scale=self.scale,
            angleCount=self.angle_count,
            angleFrom=self.angle_from,
            angleTo=self.angle_to,
        )

        # If auto_open is True, open the chart in a web browser
        if self.auto_open:
            self._open()

    def _get_asset_path(self, filename: str) -> Path:
        """Centralized asset path resolution."""
        return Path(__file__).parent / "d3_cloud_assets" / filename

    def _get_d3_js(self, path: str = "d3.min.js") -> str:
        """Retrieve the contents of the d3.js bundle.

        Args:
            path (str): The path to the d3.js file. Defaults to "d3.min.js".

        Returns:
            str: The HTML script tag containing or pointing to the d3.js script.
        """
        if path == "d3.min.js":
            path = self._get_asset_path("d3.min.js")
        path_obj = Path(path)
        try:
            return f'<script id="d3">\n{_load_local_asset(path_obj)}\n</script>'
        except FileNotFoundError:
            raise LexosException(f"Script file not found: {path}")

    def _include_d3(self) -> None:
        """Modify the template to include d3.js and d3 cloud scripts."""
        # Handle loading/initializing d3.js
        if isinstance(self.include_d3js, str) and self.include_d3js.lower() == "cdn":
            load_d3js = (
                f'<script charset="utf-8" src="https://d3js.org/d3.min.js"></script>'
            )
        elif (
            isinstance(self.include_d3js, str)
            and self.include_d3js.lower() == "directory"
        ):
            load_d3js = f'<script charset="utf-8" src="{self._get_asset_path("d3.min.js")}"></script>'
        elif isinstance(self.include_d3js, str) and self.include_d3js.endswith(".js"):
            load_d3js = self._get_d3_js(self.include_d3js)
        elif self.include_d3js is True:
            load_d3js = self._get_d3_js()
        elif self.include_d3js is False:
            load_d3js = None  # Don't include d3.js
        elif self.include_d3js is None:
            load_d3js = self._get_d3_js()
        if load_d3js:
            self.html = self.html.replace('<script id="d3"></script>', load_d3js)

    def _include_d3_cloud(self) -> None:
        """Modify the template to include d3 cloud scripts."""
        if not self.include_d3_cloud:
            return

        if self.include_d3_cloud is True:
            path = self._get_asset_path("d3cloud_bundle.min.js")
        elif isinstance(self.include_d3_cloud, str) and self.include_d3_cloud.endswith(
            ".js"
        ):
            path = Path(self.include_d3_cloud)
        else:
            return

        self.html = self.html.replace(
            '<script id="d3cloud"></script>',
            f'<script id="d3cloud">\n{_load_local_asset(path)}\n</script>',
        )

    def _load_template(self) -> str:
        """Load the HTML template for the word cloud."""
        template = self._get_asset_path(self.template)
        try:
            return _load_local_asset(template)
        except FileNotFoundError as exc:
            raise LexosException(f"Template file not found: {template}") from exc

    def _minify_html(self, html: str) -> str:
        """Basic HTML minification."""
        # Remove extra whitespace
        html = re.sub(r"\s+", " ", html)
        # Remove comments
        html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
        return html.strip()

    def _open(self) -> None:
        """Open the HTML file in a web browser."""
        # Create a temporary file to store the HTML
        with tempfile.NamedTemporaryFile(
            "w", delete=False, suffix=".html", encoding="utf-8"
        ) as temp_file:
            temp_file.write(self.html)
            temp_file_path = temp_file.name

            # Open the temporary HTML file in the default web browser
            webbrowser.open(f"file:///{temp_file_path}")

    @validate_call
    def save(self, path: Path | str, minify: bool = False) -> None:
        """Save the word cloud HTML to a file with optional HTML minification."""
        html_content = self.html

        if minify:
            html_content = self._minify_html(html_content)

        with open(path, "w", encoding="utf-8") as f:
            f.write(html_content)

angle_count: int = 5 pydantic-field ¤

The number of angles to use for the word cloud.

angle_from: int = -60 pydantic-field ¤

The starting angle for the word cloud.

angle_to: int = 60 pydantic-field ¤

The ending angle for the word cloud.

auto_open: bool = True pydantic-field ¤

Whether to open the chart in a web browser automatically.

background_color: str = 'white' pydantic-field ¤

The background color of the word cloud.

colorscale: str = 'd3.scale.category20b' pydantic-field ¤

The name of a categorical d3 scale to use for the word cloud. See https://d3js.org/d3-scale.

data: single_doc_types | multi_doc_types | pd.DataFrame pydantic-field ¤

The data to generate the word cloud from. Accepts data from a string, list of lists or tuples, a dict with terms as keys and counts/frequencies as values, or a dataframe.

docs: Optional[int | str | list[int] | list[str]] = None pydantic-field ¤

A list of documents to be selected from the DTM.

font: str = 'Impact' pydantic-field ¤

The font to use for the word cloud.

height: int = 600 pydantic-field ¤

The height of the word cloud in pixels.

html: str = '' pydantic-field ¤

The HTML representation of the word cloud.

include_d3_cloud: bool | str = True pydantic-field ¤

Whether to include the D3 cloud library. Can be a custom path to a JavaScript file or True to use the default bundled version.

include_d3js: bool | str | None = True pydantic-field ¤

Whether to include the D3.js library. Can be 'cdn', 'directory', or a custom path. If False, the D3.js library will not be included. The cloud bundle is always included unless the setting is 'directory' or False.

limit: int = 100 pydantic-field ¤

The maximum number of terms in the cloud.

scale: str = 'log' pydantic-field ¤

The scale type to use for the word cloud, 'log', 'sqrt', or 'linear'.

spiral: str = 'archimedean' pydantic-field ¤

The spiral type to use for the word cloud, 'archimedean' or 'rectangular'.

template: Path | str = 'd3_cloud_template-1.0.html' pydantic-field ¤

The template file for the word cloud.

title: str = 'Word Cloud Visualization' pydantic-field ¤

The title of the word cloud.

width: int = 600 pydantic-field ¤

The width of the word cloud in pixels.

__init__(**data: Any) -> None ¤

Initialize with better error handling.

Source code in lexos/visualization/d3_wordcloud.py
def __init__(self, **data: Any) -> None:
    """Initialize with better error handling."""
    try:
        super().__init__(**data)
    except Exception as e:
        raise LexosException(f"Failed to initialize D3WordCloud: {e}") from e

    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)
    self._render()
    self._include_d3()
    self._include_d3_cloud()

save(path: Path | str, minify: bool = False) -> None ¤

Save the word cloud HTML to a file with optional HTML minification.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def save(self, path: Path | str, minify: bool = False) -> None:
    """Save the word cloud HTML to a file with optional HTML minification."""
    html_content = self.html

    if minify:
        html_content = self._minify_html(html_content)

    with open(path, "w", encoding="utf-8") as f:
        f.write(html_content)

validate_angles() pydantic-validator ¤

Validate the angle settings.

Source code in lexos/visualization/d3_wordcloud.py
@model_validator(mode="after")
def validate_angles(self):
    """Validate the angle settings."""
    if self.angle_from >= self.angle_to:
        raise ValueError("angle_from must be less than angle_to")
    return self

validate_scale(v) pydantic-validator ¤

Validate the scale setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("scale")
@classmethod
def validate_scale(cls, v):
    """Validate the scale setting."""
    if v not in ["log", "sqrt", "linear"]:
        raise ValueError('scale must be "log", "sqrt", or "linear"')
    return v

validate_spiral(v) pydantic-validator ¤

Validate the spiral setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("spiral")
@classmethod
def validate_spiral(cls, v):
    """Validate the spiral setting."""
    if v not in ["archimedean", "rectangular"]:
        raise ValueError('spiral must be "archimedean" or "rectangular"')
    return v

__init__(**data: Any) -> None ¤

Initialize with better error handling.

Source code in lexos/visualization/d3_wordcloud.py
def __init__(self, **data: Any) -> None:
    """Initialize with better error handling."""
    try:
        super().__init__(**data)
    except Exception as e:
        raise LexosException(f"Failed to initialize D3WordCloud: {e}") from e

    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)
    self._render()
    self._include_d3()
    self._include_d3_cloud()

validate_spiral(v) pydantic-validator ¤

Validate the spiral setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("spiral")
@classmethod
def validate_spiral(cls, v):
    """Validate the spiral setting."""
    if v not in ["archimedean", "rectangular"]:
        raise ValueError('spiral must be "archimedean" or "rectangular"')
    return v

validate_scale(v) pydantic-validator ¤

Validate the scale setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("scale")
@classmethod
def validate_scale(cls, v):
    """Validate the scale setting."""
    if v not in ["log", "sqrt", "linear"]:
        raise ValueError('scale must be "log", "sqrt", or "linear"')
    return v

validate_angles() pydantic-validator ¤

Validate the angle settings.

Source code in lexos/visualization/d3_wordcloud.py
@model_validator(mode="after")
def validate_angles(self):
    """Validate the angle settings."""
    if self.angle_from >= self.angle_to:
        raise ValueError("angle_from must be less than angle_to")
    return self

_render() -> None ¤

Render the word cloud as an HTML string.

Source code in lexos/visualization/d3_wordcloud.py
def _render(self) -> None:
    """Render the word cloud as an HTML string."""
    template = Template(self._load_template())
    self.html = template.render(
        termCounts=json.dumps(self.counts),
        width=self.width,
        height=self.height,
        title=self.title,
        backgroundColor=self.background_color,
        colorscale=self.colorscale,
        font=self.font,
        spiral=self.spiral,
        scale=self.scale,
        angleCount=self.angle_count,
        angleFrom=self.angle_from,
        angleTo=self.angle_to,
    )

    # If auto_open is True, open the chart in a web browser
    if self.auto_open:
        self._open()

_get_asset_path(filename: str) -> Path ¤

Centralized asset path resolution.

Source code in lexos/visualization/d3_wordcloud.py
def _get_asset_path(self, filename: str) -> Path:
    """Centralized asset path resolution."""
    return Path(__file__).parent / "d3_cloud_assets" / filename

_get_d3_js(path: str = 'd3.min.js') -> str ¤

Retrieve the contents of the d3.js bundle.

Parameters:

Name Type Description Default
path str

The path to the d3.js file. Defaults to "d3.min.js".

'd3.min.js'

Returns:

Name Type Description
str str

The HTML script tag containing or pointing to the d3.js script.

Source code in lexos/visualization/d3_wordcloud.py
def _get_d3_js(self, path: str = "d3.min.js") -> str:
    """Retrieve the contents of the d3.js bundle.

    Args:
        path (str): The path to the d3.js file. Defaults to "d3.min.js".

    Returns:
        str: The HTML script tag containing or pointing to the d3.js script.
    """
    if path == "d3.min.js":
        path = self._get_asset_path("d3.min.js")
    path_obj = Path(path)
    try:
        return f'<script id="d3">\n{_load_local_asset(path_obj)}\n</script>'
    except FileNotFoundError:
        raise LexosException(f"Script file not found: {path}")

_include_d3() -> None ¤

Modify the template to include d3.js and d3 cloud scripts.

Source code in lexos/visualization/d3_wordcloud.py
def _include_d3(self) -> None:
    """Modify the template to include d3.js and d3 cloud scripts."""
    # Handle loading/initializing d3.js
    if isinstance(self.include_d3js, str) and self.include_d3js.lower() == "cdn":
        load_d3js = (
            f'<script charset="utf-8" src="https://d3js.org/d3.min.js"></script>'
        )
    elif (
        isinstance(self.include_d3js, str)
        and self.include_d3js.lower() == "directory"
    ):
        load_d3js = f'<script charset="utf-8" src="{self._get_asset_path("d3.min.js")}"></script>'
    elif isinstance(self.include_d3js, str) and self.include_d3js.endswith(".js"):
        load_d3js = self._get_d3_js(self.include_d3js)
    elif self.include_d3js is True:
        load_d3js = self._get_d3_js()
    elif self.include_d3js is False:
        load_d3js = None  # Don't include d3.js
    elif self.include_d3js is None:
        load_d3js = self._get_d3_js()
    if load_d3js:
        self.html = self.html.replace('<script id="d3"></script>', load_d3js)

_include_d3_cloud() -> None ¤

Modify the template to include d3 cloud scripts.

Source code in lexos/visualization/d3_wordcloud.py
def _include_d3_cloud(self) -> None:
    """Modify the template to include d3 cloud scripts."""
    if not self.include_d3_cloud:
        return

    if self.include_d3_cloud is True:
        path = self._get_asset_path("d3cloud_bundle.min.js")
    elif isinstance(self.include_d3_cloud, str) and self.include_d3_cloud.endswith(
        ".js"
    ):
        path = Path(self.include_d3_cloud)
    else:
        return

    self.html = self.html.replace(
        '<script id="d3cloud"></script>',
        f'<script id="d3cloud">\n{_load_local_asset(path)}\n</script>',
    )

_load_template() -> str ¤

Load the HTML template for the word cloud.

Source code in lexos/visualization/d3_wordcloud.py
def _load_template(self) -> str:
    """Load the HTML template for the word cloud."""
    template = self._get_asset_path(self.template)
    try:
        return _load_local_asset(template)
    except FileNotFoundError as exc:
        raise LexosException(f"Template file not found: {template}") from exc

_minify_html(html: str) -> str ¤

Basic HTML minification.

Source code in lexos/visualization/d3_wordcloud.py
def _minify_html(self, html: str) -> str:
    """Basic HTML minification."""
    # Remove extra whitespace
    html = re.sub(r"\s+", " ", html)
    # Remove comments
    html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
    return html.strip()

_open() -> None ¤

Open the HTML file in a web browser.

Source code in lexos/visualization/d3_wordcloud.py
def _open(self) -> None:
    """Open the HTML file in a web browser."""
    # Create a temporary file to store the HTML
    with tempfile.NamedTemporaryFile(
        "w", delete=False, suffix=".html", encoding="utf-8"
    ) as temp_file:
        temp_file.write(self.html)
        temp_file_path = temp_file.name

        # Open the temporary HTML file in the default web browser
        webbrowser.open(f"file:///{temp_file_path}")

save(path: Path | str, minify: bool = False) -> None ¤

Save the word cloud HTML to a file with optional HTML minification.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def save(self, path: Path | str, minify: bool = False) -> None:
    """Save the word cloud HTML to a file with optional HTML minification."""
    html_content = self.html

    if minify:
        html_content = self._minify_html(html_content)

    with open(path, "w", encoding="utf-8") as f:
        f.write(html_content)

D3MultiCloud pydantic-model ¤

Bases: BaseModel

A Pydantic model for creating multiple D3 WordClouds in a grid layout.

Config:

  • arbitrary_types_allowed: True

Fields:

Validators:

Source code in lexos/visualization/d3_wordcloud.py
class D3MultiCloud(BaseModel):
    """A Pydantic model for creating multiple D3 WordClouds in a grid layout."""

    data_sources: list[multi_doc_types] = Field(
        ...,
        description="List of data sources to create individual word clouds from.",
    )
    labels: Optional[list[str]] = Field(
        None,
        description="List of titles for each word cloud. If None, will use 'Cloud 1', 'Cloud 2', etc.",
    )
    cloud_width: int = Field(
        300, gt=50, description="The width of each individual word cloud in pixels."
    )
    cloud_height: int = Field(
        300, gt=50, description="The height of each individual word cloud in pixels."
    )
    columns: int = Field(
        3, gt=0, description="The number of columns in the grid layout."
    )
    title: Optional[str] = Field(None, description="Overall title for the figure.")
    cloud_spacing: int = Field(
        20, ge=0, description="The spacing between clouds in pixels."
    )
    limit: int = Field(50, description="The maximum number of terms in each cloud.")
    font: str = Field("Impact", description="The font to use for all word clouds.")
    spiral: str = Field(
        "archimedean",
        description="The spiral type to use for all word clouds.",
    )
    scale: str = Field(
        "log",
        description="The scale type to use for all word clouds.",
    )
    angle_count: int = Field(
        5, description="The number of angles to use for all word clouds."
    )
    angle_from: int = Field(-60, description="The starting angle for all word clouds.")
    angle_to: int = Field(60, description="The ending angle for all word clouds.")
    background_color: str = Field(
        "white", description="The background color of the overall visualization."
    )
    colorscale: str = Field(
        "d3.scale.category20b",
        description="The name of a categorical d3 scale to use for all word clouds.",
    )
    auto_open: bool = Field(
        True, description="Whether to open the chart in a web browser automatically."
    )
    template: Path | str = Field(
        "d3_multicloud_template-1.0.html",
        description="The template file for the multi-cloud visualization.",
    )
    include_d3js: bool | str | None = Field(
        True,
        description="Whether to include the D3.js library.",
    )
    include_d3_cloud: bool | str = Field(
        True,
        description="Whether to include the D3 cloud library.",
    )

    # Generated fields
    word_clouds: list[D3WordCloud] = Field(
        [], description="List of generated D3WordCloud objects."
    )
    html: str = Field("", description="The HTML representation of the multi-cloud.")

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("spiral")
    @classmethod
    def validate_spiral(cls, v):
        """Validate the spiral setting."""
        if v not in ["archimedean", "rectangular"]:
            raise LexosException('spiral must be "archimedean" or "rectangular"')
        return v

    @field_validator("scale")
    @classmethod
    def validate_scale(cls, v):
        """Validate the scale setting."""
        if v not in ["log", "sqrt", "linear"]:
            raise LexosException('scale must be "log", "sqrt", or "linear"')
        return v

    @model_validator(mode="after")
    def validate_angles(self):
        """Validate the angle settings."""
        if self.angle_from >= self.angle_to:
            raise ValueError("angle_from must be less than angle_to")
        return self

    def __init__(self, **data: Any) -> None:
        """Initialize the multi-cloud visualization."""
        try:
            super().__init__(**data)
        except Exception as e:
            raise LexosException(f"Failed to initialize D3MultiCloud: {e}") from e

        # Generate labels if not provided
        if self.labels is None:
            self.labels = [f"Doc {i + 1}" for i in range(len(self.data_sources))]
        elif len(self.labels) != len(self.data_sources):
            raise LexosException(
                "Number of labels must match number of data sources or be None"
            )

        # Generate individual word clouds
        self._generate_word_clouds()

        # Generate the combined HTML
        self._render()

    def _generate_word_clouds(self) -> None:
        """Generate individual D3WordCloud objects for each data source."""
        self.word_clouds = []

        for i, (data_source, label) in enumerate(zip(self.data_sources, self.labels)):
            cloud = D3WordCloud(
                data=data_source,
                width=self.cloud_width,
                height=self.cloud_height,
                title=label,
                limit=self.limit,
                font=self.font,
                spiral=self.spiral,
                scale=self.scale,
                angle_count=self.angle_count,
                angle_from=self.angle_from,
                angle_to=self.angle_to,
                background_color=self.background_color,
                colorscale=self.colorscale,
                auto_open=False,
                include_d3js=False,  # We'll include D3 once in the master template
                include_d3_cloud=False,  # We'll include cloud lib once in the master template
            )
            self.word_clouds.append(cloud)

    def _get_asset_path(self, filename: str) -> Path:
        """Centralized asset path resolution."""
        return Path(__file__).parent / "d3_cloud_assets" / filename

    def _get_cloud(self, index: int) -> D3WordCloud:
        """Get a specific word cloud by index."""
        if 0 <= index < len(self.word_clouds):
            return self.word_clouds[index]
        raise IndexError(f"Cloud index {index} out of range")

    def _get_d3_js(self, path: str = "d3.min.js") -> str:
        """Retrieve the contents of the d3.js bundle."""
        if path == "d3.min.js":
            path = self._get_asset_path("d3.min.js")
        try:
            with open(path) as f:
                return f'<script id="d3">\n{f.read()}\n</script>'
        except FileNotFoundError:
            raise LexosException(f"Script file not found: {path}")

    def _include_d3(self) -> None:
        """Modify the template to include d3.js."""
        if isinstance(self.include_d3js, str) and self.include_d3js.lower() == "cdn":
            load_d3js = (
                '<script charset="utf-8" src="https://d3js.org/d3.v3.min.js"></script>'
            )
        elif (
            isinstance(self.include_d3js, str)
            and self.include_d3js.lower() == "directory"
        ):
            load_d3js = f'<script charset="utf-8" src="{self._get_asset_path("d3.min.js")}"></script>'
        elif isinstance(self.include_d3js, str) and self.include_d3js.endswith(".js"):
            load_d3js = self._get_d3_js(self.include_d3js)
        elif self.include_d3js is True:
            load_d3js = self._get_d3_js()
        elif self.include_d3js is False:
            load_d3js = ""
        else:
            load_d3js = self._get_d3_js()

        self.html = self.html.replace('<script id="d3"></script>', load_d3js)

    def _include_d3_cloud(self) -> None:
        """Modify the template to include d3 cloud scripts."""
        if self.include_d3_cloud is True:
            path = "d3_cloud_assets/d3cloud_bundle.min.js"
        elif isinstance(self.include_d3_cloud, str) and self.include_d3_cloud.endswith(
            ".js"
        ):
            path = self.include_d3_cloud
        else:
            path = "d3_cloud_assets/d3cloud_bundle.min.js"

        if self.include_d3_cloud:
            try:
                with open(path) as f:
                    self.html = self.html.replace(
                        '<script id="d3cloud"></script>',
                        f'<script id="d3cloud">\n{f.read()}\n</script>',
                    )
            except FileNotFoundError:
                # Fallback to CDN
                self.html = self.html.replace(
                    '<script id="d3cloud"></script>',
                    '<script src="https://cdn.jsdelivr.net/gh/jasondavies/d3-cloud/build/d3.layout.cloud.js"></script>',
                )

    def _load_template(self) -> str:
        """Load the HTML template for the multi-cloud visualization."""
        template = self._get_asset_path(self.template)
        with open(template) as f:
            return f.read()

    def _open(self) -> None:
        """Open the HTML file in a web browser."""
        # Create a temporary file to store the HTML
        with tempfile.NamedTemporaryFile(
            "w", delete=False, suffix=".html", encoding="utf-8"
        ) as temp_file:
            temp_file.write(self.html)
            temp_file_path = temp_file.name

            # Open the temporary HTML file in the default web browser
            webbrowser.open(f"file:///{temp_file_path}")

    def _render(self) -> None:
        """Generate the combined HTML for all word clouds."""
        template = Template(self._load_template())

        # Calculate grid dimensions
        rows = (len(self.word_clouds) + self.columns - 1) // self.columns
        total_width = (self.cloud_width * self.columns) + (
            self.cloud_spacing * (self.columns - 1)
        )
        total_height = (self.cloud_height * rows) + (self.cloud_spacing * (rows - 1))

        # Prepare cloud data for template
        cloud_data = []
        for i, cloud in enumerate(self.word_clouds):
            row = i // self.columns
            col = i % self.columns
            x_pos = col * (self.cloud_width + self.cloud_spacing)
            y_pos = row * (self.cloud_height + self.cloud_spacing)

            cloud_data.append(
                {
                    "id": f"cloud_{i}",
                    "title": cloud.title,
                    "termCounts": cloud.counts,
                    "x": x_pos,
                    "y": y_pos,
                    "width": self.cloud_width,
                    "height": self.cloud_height,
                }
            )

        self.html = template.render(
            title=self.title,
            total_width=total_width,
            total_height=total_height + 100,  # Extra space for title
            cloud_data=json.dumps(cloud_data),
            font=self.font,
            spiral=self.spiral,
            scale=self.scale,
            angleCount=self.angle_count,
            angleFrom=self.angle_from,
            angleTo=self.angle_to,
            backgroundColor=self.background_color,
            colorscale=self.colorscale,
        )

        # Include D3 libraries
        self._include_d3()
        self._include_d3_cloud()

        # If auto_open is True, open the chart in a web browser
        if self.auto_open:
            self._open()

    @validate_call
    def get_cloud_counts(self, index: int) -> dict[str, int]:
        """Get word counts for a specific cloud by index."""
        return self._get_cloud(index).counts

    @validate_call
    def save(self, path: Path | str, minify: bool = False) -> None:
        """Save the multi-cloud HTML to a file."""
        html_content = self.html

        if minify:
            import re

            html_content = re.sub(r"\s+", " ", html_content)
            html_content = re.sub(r"<!--.*?-->", "", html_content, flags=re.DOTALL)
            html_content = html_content.strip()

        with open(path, "w", encoding="utf-8") as f:
            f.write(html_content)

angle_count: int = 5 pydantic-field ¤

The number of angles to use for all word clouds.

angle_from: int = -60 pydantic-field ¤

The starting angle for all word clouds.

angle_to: int = 60 pydantic-field ¤

The ending angle for all word clouds.

auto_open: bool = True pydantic-field ¤

Whether to open the chart in a web browser automatically.

background_color: str = 'white' pydantic-field ¤

The background color of the overall visualization.

cloud_height: int = 300 pydantic-field ¤

The height of each individual word cloud in pixels.

cloud_spacing: int = 20 pydantic-field ¤

The spacing between clouds in pixels.

cloud_width: int = 300 pydantic-field ¤

The width of each individual word cloud in pixels.

colorscale: str = 'd3.scale.category20b' pydantic-field ¤

The name of a categorical d3 scale to use for all word clouds.

columns: int = 3 pydantic-field ¤

The number of columns in the grid layout.

data_sources: list[multi_doc_types] pydantic-field ¤

List of data sources to create individual word clouds from.

font: str = 'Impact' pydantic-field ¤

The font to use for all word clouds.

html: str = '' pydantic-field ¤

The HTML representation of the multi-cloud.

include_d3_cloud: bool | str = True pydantic-field ¤

Whether to include the D3 cloud library.

include_d3js: bool | str | None = True pydantic-field ¤

Whether to include the D3.js library.

labels: Optional[list[str]] = None pydantic-field ¤

List of titles for each word cloud. If None, will use 'Cloud 1', 'Cloud 2', etc.

limit: int = 50 pydantic-field ¤

The maximum number of terms in each cloud.

scale: str = 'log' pydantic-field ¤

The scale type to use for all word clouds.

spiral: str = 'archimedean' pydantic-field ¤

The spiral type to use for all word clouds.

template: Path | str = 'd3_multicloud_template-1.0.html' pydantic-field ¤

The template file for the multi-cloud visualization.

title: Optional[str] = None pydantic-field ¤

Overall title for the figure.

word_clouds: list[D3WordCloud] = [] pydantic-field ¤

List of generated D3WordCloud objects.

__init__(**data: Any) -> None ¤

Initialize the multi-cloud visualization.

Source code in lexos/visualization/d3_wordcloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the multi-cloud visualization."""
    try:
        super().__init__(**data)
    except Exception as e:
        raise LexosException(f"Failed to initialize D3MultiCloud: {e}") from e

    # Generate labels if not provided
    if self.labels is None:
        self.labels = [f"Doc {i + 1}" for i in range(len(self.data_sources))]
    elif len(self.labels) != len(self.data_sources):
        raise LexosException(
            "Number of labels must match number of data sources or be None"
        )

    # Generate individual word clouds
    self._generate_word_clouds()

    # Generate the combined HTML
    self._render()

get_cloud_counts(index: int) -> dict[str, int] ¤

Get word counts for a specific cloud by index.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def get_cloud_counts(self, index: int) -> dict[str, int]:
    """Get word counts for a specific cloud by index."""
    return self._get_cloud(index).counts

save(path: Path | str, minify: bool = False) -> None ¤

Save the multi-cloud HTML to a file.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def save(self, path: Path | str, minify: bool = False) -> None:
    """Save the multi-cloud HTML to a file."""
    html_content = self.html

    if minify:
        import re

        html_content = re.sub(r"\s+", " ", html_content)
        html_content = re.sub(r"<!--.*?-->", "", html_content, flags=re.DOTALL)
        html_content = html_content.strip()

    with open(path, "w", encoding="utf-8") as f:
        f.write(html_content)

validate_angles() pydantic-validator ¤

Validate the angle settings.

Source code in lexos/visualization/d3_wordcloud.py
@model_validator(mode="after")
def validate_angles(self):
    """Validate the angle settings."""
    if self.angle_from >= self.angle_to:
        raise ValueError("angle_from must be less than angle_to")
    return self

validate_scale(v) pydantic-validator ¤

Validate the scale setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("scale")
@classmethod
def validate_scale(cls, v):
    """Validate the scale setting."""
    if v not in ["log", "sqrt", "linear"]:
        raise LexosException('scale must be "log", "sqrt", or "linear"')
    return v

validate_spiral(v) pydantic-validator ¤

Validate the spiral setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("spiral")
@classmethod
def validate_spiral(cls, v):
    """Validate the spiral setting."""
    if v not in ["archimedean", "rectangular"]:
        raise LexosException('spiral must be "archimedean" or "rectangular"')
    return v

__init__(**data: Any) -> None ¤

Initialize the multi-cloud visualization.

Source code in lexos/visualization/d3_wordcloud.py
def __init__(self, **data: Any) -> None:
    """Initialize the multi-cloud visualization."""
    try:
        super().__init__(**data)
    except Exception as e:
        raise LexosException(f"Failed to initialize D3MultiCloud: {e}") from e

    # Generate labels if not provided
    if self.labels is None:
        self.labels = [f"Doc {i + 1}" for i in range(len(self.data_sources))]
    elif len(self.labels) != len(self.data_sources):
        raise LexosException(
            "Number of labels must match number of data sources or be None"
        )

    # Generate individual word clouds
    self._generate_word_clouds()

    # Generate the combined HTML
    self._render()

validate_spiral(v) pydantic-validator ¤

Validate the spiral setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("spiral")
@classmethod
def validate_spiral(cls, v):
    """Validate the spiral setting."""
    if v not in ["archimedean", "rectangular"]:
        raise LexosException('spiral must be "archimedean" or "rectangular"')
    return v

validate_scale(v) pydantic-validator ¤

Validate the scale setting.

Source code in lexos/visualization/d3_wordcloud.py
@field_validator("scale")
@classmethod
def validate_scale(cls, v):
    """Validate the scale setting."""
    if v not in ["log", "sqrt", "linear"]:
        raise LexosException('scale must be "log", "sqrt", or "linear"')
    return v

validate_angles() pydantic-validator ¤

Validate the angle settings.

Source code in lexos/visualization/d3_wordcloud.py
@model_validator(mode="after")
def validate_angles(self):
    """Validate the angle settings."""
    if self.angle_from >= self.angle_to:
        raise ValueError("angle_from must be less than angle_to")
    return self

_generate_word_clouds() -> None ¤

Generate individual D3WordCloud objects for each data source.

Source code in lexos/visualization/d3_wordcloud.py
def _generate_word_clouds(self) -> None:
    """Generate individual D3WordCloud objects for each data source."""
    self.word_clouds = []

    for i, (data_source, label) in enumerate(zip(self.data_sources, self.labels)):
        cloud = D3WordCloud(
            data=data_source,
            width=self.cloud_width,
            height=self.cloud_height,
            title=label,
            limit=self.limit,
            font=self.font,
            spiral=self.spiral,
            scale=self.scale,
            angle_count=self.angle_count,
            angle_from=self.angle_from,
            angle_to=self.angle_to,
            background_color=self.background_color,
            colorscale=self.colorscale,
            auto_open=False,
            include_d3js=False,  # We'll include D3 once in the master template
            include_d3_cloud=False,  # We'll include cloud lib once in the master template
        )
        self.word_clouds.append(cloud)

_get_asset_path(filename: str) -> Path ¤

Centralized asset path resolution.

Source code in lexos/visualization/d3_wordcloud.py
def _get_asset_path(self, filename: str) -> Path:
    """Centralized asset path resolution."""
    return Path(__file__).parent / "d3_cloud_assets" / filename

_get_cloud(index: int) -> D3WordCloud ¤

Get a specific word cloud by index.

Source code in lexos/visualization/d3_wordcloud.py
def _get_cloud(self, index: int) -> D3WordCloud:
    """Get a specific word cloud by index."""
    if 0 <= index < len(self.word_clouds):
        return self.word_clouds[index]
    raise IndexError(f"Cloud index {index} out of range")

_get_d3_js(path: str = 'd3.min.js') -> str ¤

Retrieve the contents of the d3.js bundle.

Source code in lexos/visualization/d3_wordcloud.py
def _get_d3_js(self, path: str = "d3.min.js") -> str:
    """Retrieve the contents of the d3.js bundle."""
    if path == "d3.min.js":
        path = self._get_asset_path("d3.min.js")
    try:
        with open(path) as f:
            return f'<script id="d3">\n{f.read()}\n</script>'
    except FileNotFoundError:
        raise LexosException(f"Script file not found: {path}")

_include_d3() -> None ¤

Modify the template to include d3.js.

Source code in lexos/visualization/d3_wordcloud.py
def _include_d3(self) -> None:
    """Modify the template to include d3.js."""
    if isinstance(self.include_d3js, str) and self.include_d3js.lower() == "cdn":
        load_d3js = (
            '<script charset="utf-8" src="https://d3js.org/d3.v3.min.js"></script>'
        )
    elif (
        isinstance(self.include_d3js, str)
        and self.include_d3js.lower() == "directory"
    ):
        load_d3js = f'<script charset="utf-8" src="{self._get_asset_path("d3.min.js")}"></script>'
    elif isinstance(self.include_d3js, str) and self.include_d3js.endswith(".js"):
        load_d3js = self._get_d3_js(self.include_d3js)
    elif self.include_d3js is True:
        load_d3js = self._get_d3_js()
    elif self.include_d3js is False:
        load_d3js = ""
    else:
        load_d3js = self._get_d3_js()

    self.html = self.html.replace('<script id="d3"></script>', load_d3js)

_include_d3_cloud() -> None ¤

Modify the template to include d3 cloud scripts.

Source code in lexos/visualization/d3_wordcloud.py
def _include_d3_cloud(self) -> None:
    """Modify the template to include d3 cloud scripts."""
    if self.include_d3_cloud is True:
        path = "d3_cloud_assets/d3cloud_bundle.min.js"
    elif isinstance(self.include_d3_cloud, str) and self.include_d3_cloud.endswith(
        ".js"
    ):
        path = self.include_d3_cloud
    else:
        path = "d3_cloud_assets/d3cloud_bundle.min.js"

    if self.include_d3_cloud:
        try:
            with open(path) as f:
                self.html = self.html.replace(
                    '<script id="d3cloud"></script>',
                    f'<script id="d3cloud">\n{f.read()}\n</script>',
                )
        except FileNotFoundError:
            # Fallback to CDN
            self.html = self.html.replace(
                '<script id="d3cloud"></script>',
                '<script src="https://cdn.jsdelivr.net/gh/jasondavies/d3-cloud/build/d3.layout.cloud.js"></script>',
            )

_load_template() -> str ¤

Load the HTML template for the multi-cloud visualization.

Source code in lexos/visualization/d3_wordcloud.py
def _load_template(self) -> str:
    """Load the HTML template for the multi-cloud visualization."""
    template = self._get_asset_path(self.template)
    with open(template) as f:
        return f.read()

_open() -> None ¤

Open the HTML file in a web browser.

Source code in lexos/visualization/d3_wordcloud.py
def _open(self) -> None:
    """Open the HTML file in a web browser."""
    # Create a temporary file to store the HTML
    with tempfile.NamedTemporaryFile(
        "w", delete=False, suffix=".html", encoding="utf-8"
    ) as temp_file:
        temp_file.write(self.html)
        temp_file_path = temp_file.name

        # Open the temporary HTML file in the default web browser
        webbrowser.open(f"file:///{temp_file_path}")

_render() -> None ¤

Generate the combined HTML for all word clouds.

Source code in lexos/visualization/d3_wordcloud.py
def _render(self) -> None:
    """Generate the combined HTML for all word clouds."""
    template = Template(self._load_template())

    # Calculate grid dimensions
    rows = (len(self.word_clouds) + self.columns - 1) // self.columns
    total_width = (self.cloud_width * self.columns) + (
        self.cloud_spacing * (self.columns - 1)
    )
    total_height = (self.cloud_height * rows) + (self.cloud_spacing * (rows - 1))

    # Prepare cloud data for template
    cloud_data = []
    for i, cloud in enumerate(self.word_clouds):
        row = i // self.columns
        col = i % self.columns
        x_pos = col * (self.cloud_width + self.cloud_spacing)
        y_pos = row * (self.cloud_height + self.cloud_spacing)

        cloud_data.append(
            {
                "id": f"cloud_{i}",
                "title": cloud.title,
                "termCounts": cloud.counts,
                "x": x_pos,
                "y": y_pos,
                "width": self.cloud_width,
                "height": self.cloud_height,
            }
        )

    self.html = template.render(
        title=self.title,
        total_width=total_width,
        total_height=total_height + 100,  # Extra space for title
        cloud_data=json.dumps(cloud_data),
        font=self.font,
        spiral=self.spiral,
        scale=self.scale,
        angleCount=self.angle_count,
        angleFrom=self.angle_from,
        angleTo=self.angle_to,
        backgroundColor=self.background_color,
        colorscale=self.colorscale,
    )

    # Include D3 libraries
    self._include_d3()
    self._include_d3_cloud()

    # If auto_open is True, open the chart in a web browser
    if self.auto_open:
        self._open()

get_cloud_counts(index: int) -> dict[str, int] ¤

Get word counts for a specific cloud by index.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def get_cloud_counts(self, index: int) -> dict[str, int]:
    """Get word counts for a specific cloud by index."""
    return self._get_cloud(index).counts

save(path: Path | str, minify: bool = False) -> None ¤

Save the multi-cloud HTML to a file.

Source code in lexos/visualization/d3_wordcloud.py
@validate_call
def save(self, path: Path | str, minify: bool = False) -> None:
    """Save the multi-cloud HTML to a file."""
    html_content = self.html

    if minify:
        import re

        html_content = re.sub(r"\s+", " ", html_content)
        html_content = re.sub(r"<!--.*?-->", "", html_content, flags=re.DOTALL)
        html_content = html_content.strip()

    with open(path, "w", encoding="utf-8") as f:
        f.write(html_content)