Skip to content

Bubble Visualizations¤

Static Bubble Visualizations¤

BubbleChart pydantic-model ¤

Bases: BaseModel

Bubble chart.

Notes: - If the counts are sorted, the results might look weird. - If "limit" is raised too high, it will take a long time to generate the plot - Based on https://matplotlib.org/stable/gallery/misc/packed_bubbles.html.

Config:

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

Fields:

Validators:

Source code in lexos/visualization/bubbleviz.py
class BubbleChart(BaseModel):
    """Bubble chart.

    Notes:
    - If the counts are sorted, the results might look weird.
    - If "limit" is raised too high, it will take a long time to generate the plot
    - Based on https://matplotlib.org/stable/gallery/misc/packed_bubbles.html.
    """

    data: Optional[single_doc_types | multi_doc_types | pd.DataFrame] = Field(
        description="The data to plot."
    )
    docs: Optional[int | str | list[int] | list[str]] = Field(
        None, description="The document indices or labels to plot."
    )
    limit: Optional[int] = Field(
        100, description="The maximum number of bubbles to plot."
    )
    title: Optional[str] = Field(None, description="The title of the plot.")
    bubble_spacing: Optional[float | int] = Field(
        0.1, description="The spacing between bubbles."
    )
    colors: Optional[list[str]] = Field(
        DEFAULT_COLORS, description="The colors of the bubbles."
    )
    figsize: Optional[int | float] = Field(
        10, description="The size of the figure in inches."
    )
    font_family: Optional[str] = Field(
        "DejaVu Sans", description="The font family of the plot."
    )
    showfig: Optional[bool] = Field(True, description="Whether to show the plot.")
    bubbles: Optional[np.ndarray] = Field(None, description="The bubbles.")
    maxstep: Optional[int] = Field(None, description="The maximum step.")
    step_dist: Optional[int] = Field(None, description="The step distance.")
    com: Optional[int] = Field(None, description="The center of mass.")
    counts: dict[str, int] = Field({}, description="A dictionary of word counts.")
    fig: Optional[plt.Figure] = Field(
        None, description="The figure for the bubble chart."
    )

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

    @field_validator("data", mode="after")
    @classmethod
    def is_not_empty(cls, value: Any) -> Any:
        """Check if the value is not empty."""
        if isinstance(value, pd.DataFrame):
            if value.empty:
                raise LexosException("Dataframe is empty.")
            return value

        if value == "" or value == [] or value == {}:
            raise LexosException("Data is an empty list or string.")
        return value

    def __init__(self, **data):
        """Initialize the BubbleChart with the provided data."""
        super().__init__(**data)

        # Process different data types to get individual document data
        self.counts = processors.process_data(self.data, self.docs, self.limit)

        # Set the figure dimensions
        self.figsize = (self.figsize, self.figsize)

        # Reduce the area to the limited number of terms
        area = np.asarray(list(self.counts.values()))
        r = np.sqrt(area / np.pi)

        self.bubbles = np.ones((len(area), 4))
        self.bubbles[:, 2] = r
        self.bubbles[:, 3] = area
        self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing
        self.step_dist = self.maxstep / 2

        # Calculate initial grid layout for bubbles
        length = np.ceil(np.sqrt(len(self.bubbles)))
        grid = np.arange(length) * self.maxstep
        gx, gy = np.meshgrid(grid, grid)
        self.bubbles[:, 0] = gx.flatten()[: len(self.bubbles)]
        self.bubbles[:, 1] = gy.flatten()[: len(self.bubbles)]

        self.com = self._center_of_mass()

        # Create the figure
        self._collapse()
        fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"), figsize=self.figsize)
        self._plot(ax, list(self.counts.keys()))
        ax.axis("off")
        ax.relim()
        ax.autoscale_view()

        # Add title
        if self.title:
            ax.set_title(self.title)

        # Save the fig variable
        self.fig = fig

        plt.close()

    def _center_distance(self, bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray:
        """Centre distance.

        Args:
            bubble (np.ndarray): Bubble array.
            bubbles (np.ndarray): Bubble array.

        Returns:
            np.ndarray: The centre distance.
        """
        return np.hypot(bubble[0] - bubbles[:, 0], bubble[1] - bubbles[:, 1])

    def _center_of_mass(self) -> int:
        """Centre of mass.

        Returns:
            int: The centre of mass.
        """
        return np.average(self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3])

    def _check_collisions(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
        """Check collisions.

        Args:
            bubble (np.ndarray): Bubble array.
            bubbles (np.ndarray): Bubble array.

        Returns:
            int: The number of overlapping bubbles.
        """
        distance = self._outline_distance(bubble, bubbles)
        return int((distance < 0).sum())

    def _collapse(self, n_iterations: int = 50):
        """Move bubbles to the center of mass.

        Args:
            n_iterations (int): Number of moves to perform.
        """
        num_bubbles = len(self.bubbles)
        for _i in range(n_iterations):
            moves = 0
            for i in range(num_bubbles):
                mask = np.ones(num_bubbles, dtype=bool)
                mask[i] = False
                rest_bub = self.bubbles[mask]

                # Try to move directly towards the center of mass
                dir_vec = self.com - self.bubbles[i, :2]
                dir_vec_magnitude = np.hypot(dir_vec[0], dir_vec[1])
                if dir_vec_magnitude > 0:
                    dir_vec = dir_vec / dir_vec_magnitude
                else:
                    dir_vec = np.array([1.0, 0.0], dtype=float) * self.step_dist * 0.01

                # Calculate new bubble position
                new_point = self.bubbles[i, :2] + dir_vec * self.step_dist
                new_bubble = np.append(new_point, self.bubbles[i, 2:4])

                if self._check_collisions(new_bubble, rest_bub) == 0:
                    self.bubbles[i, :] = new_bubble
                    self.com = self._center_of_mass()
                    moves += 1
                    continue

                for colliding in self._collides_with(new_bubble, rest_bub):
                    dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
                    dir_vec_magnitude = np.hypot(dir_vec[0], dir_vec[1])
                    if dir_vec_magnitude == 0:
                        continue
                    dir_vec = dir_vec / dir_vec_magnitude
                    orth = np.array([dir_vec[1], -dir_vec[0]], dtype=float)
                    new_point1 = self.bubbles[i, :2] + orth * self.step_dist
                    new_point2 = self.bubbles[i, :2] - orth * self.step_dist
                    dist1 = self._center_distance(self.com, np.array([new_point1]))
                    dist2 = self._center_distance(self.com, np.array([new_point2]))
                    new_point = new_point1 if dist1 < dist2 else new_point2
                    new_bubble = np.append(new_point, self.bubbles[i, 2:4])
                    if self._check_collisions(new_bubble, rest_bub) == 0:
                        self.bubbles[i, :] = new_bubble
                        self.com = self._center_of_mass()
                        moves += 1
                        break

            if moves == 0:
                break
            if moves / num_bubbles < 0.1:
                self.step_dist = self.step_dist / 2

    def _collides_with(self, bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray:
        """Return indices of overlapping bubbles.

        Args:
            bubble (np.ndarray): Bubble array.
            bubbles (np.ndarray): Bubble array.

        Returns:
            np.ndarray: Indices of overlapping bubbles.
        """
        distance = self._outline_distance(bubble, bubbles)
        return np.where(distance < 0)[0]

    def _outline_distance(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
        """Outline distance.

        Args:
            bubble (np.ndarray): Bubble array.
            bubbles (np.ndarray): Bubble array.

        Returns:
            int: The outline distance.
        """
        center_distance = self._center_distance(bubble, bubbles)
        return center_distance - bubble[2] - bubbles[:, 2] - self.bubble_spacing

    def _plot(
        self,
        ax: Axes,
        labels: list[str],
    ):
        """Draw the bubble plot.

        Args:
            ax (Axes): The matplotlib Axes.
            labels (list[str]): The labels of the bubbles.
        """
        color_num = 0
        for i in range(len(self.bubbles)):
            if color_num == len(self.colors) - 1:
                color_num = 0
            else:
                color_num += 1
            circ = plt.Circle(
                self.bubbles[i, :2], self.bubbles[i, 2], color=self.colors[color_num]
            )
            ax.add_patch(circ)
            ax.text(
                *self.bubbles[i, :2],
                labels[i],
                horizontalalignment="center",
                verticalalignment="center",
                fontfamily=self.font_family,
            )

    @validate_call(config=model_config)
    def save(self, path: Path | str, **kwargs: Any):
        """Save the figure as a file.

        Args:
            path (Path | str): The path to the file to save.
            **kwargs (Any): Additional keyword arguments for `plt.savefig`.
        """
        if path == "":
            raise LexosException("You must provide a valid path.")
        if self.fig is None:
            raise LexosException("The figure has not yet been generated.")
        self.fig.savefig(path, **kwargs)

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

        This is a helper method. You can also reference the figure using
        `BubbleChart.fig`. This will generally display in a Jupyter notebook.
        """
        return self.fig

bubble_spacing: Optional[float | int] = 0.1 pydantic-field ¤

The spacing between bubbles.

colors: Optional[list[str]] = DEFAULT_COLORS pydantic-field ¤

The colors of the bubbles.

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

The data to plot.

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

The document indices or labels to plot.

font_family: Optional[str] = 'DejaVu Sans' pydantic-field ¤

The font family of the plot.

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

The maximum number of bubbles to plot.

showfig: Optional[bool] = True pydantic-field ¤

Whether to show the plot.

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

The title of the plot.

__init__(**data) ¤

Initialize the BubbleChart with the provided data.

Source code in lexos/visualization/bubbleviz.py
def __init__(self, **data):
    """Initialize the BubbleChart with the provided data."""
    super().__init__(**data)

    # Process different data types to get individual document data
    self.counts = processors.process_data(self.data, self.docs, self.limit)

    # Set the figure dimensions
    self.figsize = (self.figsize, self.figsize)

    # Reduce the area to the limited number of terms
    area = np.asarray(list(self.counts.values()))
    r = np.sqrt(area / np.pi)

    self.bubbles = np.ones((len(area), 4))
    self.bubbles[:, 2] = r
    self.bubbles[:, 3] = area
    self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing
    self.step_dist = self.maxstep / 2

    # Calculate initial grid layout for bubbles
    length = np.ceil(np.sqrt(len(self.bubbles)))
    grid = np.arange(length) * self.maxstep
    gx, gy = np.meshgrid(grid, grid)
    self.bubbles[:, 0] = gx.flatten()[: len(self.bubbles)]
    self.bubbles[:, 1] = gy.flatten()[: len(self.bubbles)]

    self.com = self._center_of_mass()

    # Create the figure
    self._collapse()
    fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"), figsize=self.figsize)
    self._plot(ax, list(self.counts.keys()))
    ax.axis("off")
    ax.relim()
    ax.autoscale_view()

    # Add title
    if self.title:
        ax.set_title(self.title)

    # Save the fig variable
    self.fig = fig

    plt.close()

is_not_empty(value: Any) -> Any pydantic-validator ¤

Check if the value is not empty.

Source code in lexos/visualization/bubbleviz.py
@field_validator("data", mode="after")
@classmethod
def is_not_empty(cls, value: Any) -> Any:
    """Check if the value is not empty."""
    if isinstance(value, pd.DataFrame):
        if value.empty:
            raise LexosException("Dataframe is empty.")
        return value

    if value == "" or value == [] or value == {}:
        raise LexosException("Data is an empty list or string.")
    return value

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

Save the figure as a file.

Parameters:

Name Type Description Default
path Path | str

The path to the file to save.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/bubbleviz.py
@validate_call(config=model_config)
def save(self, path: Path | str, **kwargs: Any):
    """Save the figure as a file.

    Args:
        path (Path | str): The path to the file to save.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if path == "":
        raise LexosException("You must provide a valid path.")
    if self.fig is None:
        raise LexosException("The figure has not yet been generated.")
    self.fig.savefig(path, **kwargs)

show() ¤

Show the figure if it is hidden.

This is a helper method. You can also reference the figure using BubbleChart.fig. This will generally display in a Jupyter notebook.

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

    This is a helper method. You can also reference the figure using
    `BubbleChart.fig`. This will generally display in a Jupyter notebook.
    """
    return self.fig

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

The data to plot.

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

The document indices or labels to plot.

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

The maximum number of bubbles to plot.

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

The title of the plot.

bubble_spacing: Optional[float | int] = 0.1 pydantic-field ¤

The spacing between bubbles.

colors: Optional[list[str]] = DEFAULT_COLORS pydantic-field ¤

The colors of the bubbles.

figsize: Optional[int | float] = (self.figsize, self.figsize) pydantic-field ¤

font_family: Optional[str] = 'DejaVu Sans' pydantic-field ¤

The font family of the plot.

showfig: Optional[bool] = True pydantic-field ¤

Whether to show the plot.

bubbles: Optional[np.ndarray] pydantic-field ¤

maxstep: Optional[int] = 2 * self.bubbles[:, 2].max() + self.bubble_spacing pydantic-field ¤

step_dist: Optional[int] = self.maxstep / 2 pydantic-field ¤

com: Optional[int] pydantic-field ¤

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

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

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

is_not_empty(value: Any) -> Any pydantic-validator ¤

Check if the value is not empty.

Source code in lexos/visualization/bubbleviz.py
@field_validator("data", mode="after")
@classmethod
def is_not_empty(cls, value: Any) -> Any:
    """Check if the value is not empty."""
    if isinstance(value, pd.DataFrame):
        if value.empty:
            raise LexosException("Dataframe is empty.")
        return value

    if value == "" or value == [] or value == {}:
        raise LexosException("Data is an empty list or string.")
    return value

__init__(**data) ¤

Initialize the BubbleChart with the provided data.

Source code in lexos/visualization/bubbleviz.py
def __init__(self, **data):
    """Initialize the BubbleChart with the provided data."""
    super().__init__(**data)

    # Process different data types to get individual document data
    self.counts = processors.process_data(self.data, self.docs, self.limit)

    # Set the figure dimensions
    self.figsize = (self.figsize, self.figsize)

    # Reduce the area to the limited number of terms
    area = np.asarray(list(self.counts.values()))
    r = np.sqrt(area / np.pi)

    self.bubbles = np.ones((len(area), 4))
    self.bubbles[:, 2] = r
    self.bubbles[:, 3] = area
    self.maxstep = 2 * self.bubbles[:, 2].max() + self.bubble_spacing
    self.step_dist = self.maxstep / 2

    # Calculate initial grid layout for bubbles
    length = np.ceil(np.sqrt(len(self.bubbles)))
    grid = np.arange(length) * self.maxstep
    gx, gy = np.meshgrid(grid, grid)
    self.bubbles[:, 0] = gx.flatten()[: len(self.bubbles)]
    self.bubbles[:, 1] = gy.flatten()[: len(self.bubbles)]

    self.com = self._center_of_mass()

    # Create the figure
    self._collapse()
    fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"), figsize=self.figsize)
    self._plot(ax, list(self.counts.keys()))
    ax.axis("off")
    ax.relim()
    ax.autoscale_view()

    # Add title
    if self.title:
        ax.set_title(self.title)

    # Save the fig variable
    self.fig = fig

    plt.close()

_center_distance(bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray ¤

Centre distance.

Parameters:

Name Type Description Default
bubble ndarray

Bubble array.

required
bubbles ndarray

Bubble array.

required

Returns:

Type Description
ndarray

np.ndarray: The centre distance.

Source code in lexos/visualization/bubbleviz.py
def _center_distance(self, bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray:
    """Centre distance.

    Args:
        bubble (np.ndarray): Bubble array.
        bubbles (np.ndarray): Bubble array.

    Returns:
        np.ndarray: The centre distance.
    """
    return np.hypot(bubble[0] - bubbles[:, 0], bubble[1] - bubbles[:, 1])

_center_of_mass() -> int ¤

Centre of mass.

Returns:

Name Type Description
int int

The centre of mass.

Source code in lexos/visualization/bubbleviz.py
def _center_of_mass(self) -> int:
    """Centre of mass.

    Returns:
        int: The centre of mass.
    """
    return np.average(self.bubbles[:, :2], axis=0, weights=self.bubbles[:, 3])

_check_collisions(bubble: np.ndarray, bubbles: np.ndarray) -> int ¤

Check collisions.

Parameters:

Name Type Description Default
bubble ndarray

Bubble array.

required
bubbles ndarray

Bubble array.

required

Returns:

Name Type Description
int int

The number of overlapping bubbles.

Source code in lexos/visualization/bubbleviz.py
def _check_collisions(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
    """Check collisions.

    Args:
        bubble (np.ndarray): Bubble array.
        bubbles (np.ndarray): Bubble array.

    Returns:
        int: The number of overlapping bubbles.
    """
    distance = self._outline_distance(bubble, bubbles)
    return int((distance < 0).sum())

_collapse(n_iterations: int = 50) ¤

Move bubbles to the center of mass.

Parameters:

Name Type Description Default
n_iterations int

Number of moves to perform.

50
Source code in lexos/visualization/bubbleviz.py
def _collapse(self, n_iterations: int = 50):
    """Move bubbles to the center of mass.

    Args:
        n_iterations (int): Number of moves to perform.
    """
    num_bubbles = len(self.bubbles)
    for _i in range(n_iterations):
        moves = 0
        for i in range(num_bubbles):
            mask = np.ones(num_bubbles, dtype=bool)
            mask[i] = False
            rest_bub = self.bubbles[mask]

            # Try to move directly towards the center of mass
            dir_vec = self.com - self.bubbles[i, :2]
            dir_vec_magnitude = np.hypot(dir_vec[0], dir_vec[1])
            if dir_vec_magnitude > 0:
                dir_vec = dir_vec / dir_vec_magnitude
            else:
                dir_vec = np.array([1.0, 0.0], dtype=float) * self.step_dist * 0.01

            # Calculate new bubble position
            new_point = self.bubbles[i, :2] + dir_vec * self.step_dist
            new_bubble = np.append(new_point, self.bubbles[i, 2:4])

            if self._check_collisions(new_bubble, rest_bub) == 0:
                self.bubbles[i, :] = new_bubble
                self.com = self._center_of_mass()
                moves += 1
                continue

            for colliding in self._collides_with(new_bubble, rest_bub):
                dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
                dir_vec_magnitude = np.hypot(dir_vec[0], dir_vec[1])
                if dir_vec_magnitude == 0:
                    continue
                dir_vec = dir_vec / dir_vec_magnitude
                orth = np.array([dir_vec[1], -dir_vec[0]], dtype=float)
                new_point1 = self.bubbles[i, :2] + orth * self.step_dist
                new_point2 = self.bubbles[i, :2] - orth * self.step_dist
                dist1 = self._center_distance(self.com, np.array([new_point1]))
                dist2 = self._center_distance(self.com, np.array([new_point2]))
                new_point = new_point1 if dist1 < dist2 else new_point2
                new_bubble = np.append(new_point, self.bubbles[i, 2:4])
                if self._check_collisions(new_bubble, rest_bub) == 0:
                    self.bubbles[i, :] = new_bubble
                    self.com = self._center_of_mass()
                    moves += 1
                    break

        if moves == 0:
            break
        if moves / num_bubbles < 0.1:
            self.step_dist = self.step_dist / 2

_collides_with(bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray ¤

Return indices of overlapping bubbles.

Parameters:

Name Type Description Default
bubble ndarray

Bubble array.

required
bubbles ndarray

Bubble array.

required

Returns:

Type Description
ndarray

np.ndarray: Indices of overlapping bubbles.

Source code in lexos/visualization/bubbleviz.py
def _collides_with(self, bubble: np.ndarray, bubbles: np.ndarray) -> np.ndarray:
    """Return indices of overlapping bubbles.

    Args:
        bubble (np.ndarray): Bubble array.
        bubbles (np.ndarray): Bubble array.

    Returns:
        np.ndarray: Indices of overlapping bubbles.
    """
    distance = self._outline_distance(bubble, bubbles)
    return np.where(distance < 0)[0]

_outline_distance(bubble: np.ndarray, bubbles: np.ndarray) -> int ¤

Outline distance.

Parameters:

Name Type Description Default
bubble ndarray

Bubble array.

required
bubbles ndarray

Bubble array.

required

Returns:

Name Type Description
int int

The outline distance.

Source code in lexos/visualization/bubbleviz.py
def _outline_distance(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
    """Outline distance.

    Args:
        bubble (np.ndarray): Bubble array.
        bubbles (np.ndarray): Bubble array.

    Returns:
        int: The outline distance.
    """
    center_distance = self._center_distance(bubble, bubbles)
    return center_distance - bubble[2] - bubbles[:, 2] - self.bubble_spacing

_plot(ax: Axes, labels: list[str]) ¤

Draw the bubble plot.

Parameters:

Name Type Description Default
ax Axes

The matplotlib Axes.

required
labels list[str]

The labels of the bubbles.

required
Source code in lexos/visualization/bubbleviz.py
def _plot(
    self,
    ax: Axes,
    labels: list[str],
):
    """Draw the bubble plot.

    Args:
        ax (Axes): The matplotlib Axes.
        labels (list[str]): The labels of the bubbles.
    """
    color_num = 0
    for i in range(len(self.bubbles)):
        if color_num == len(self.colors) - 1:
            color_num = 0
        else:
            color_num += 1
        circ = plt.Circle(
            self.bubbles[i, :2], self.bubbles[i, 2], color=self.colors[color_num]
        )
        ax.add_patch(circ)
        ax.text(
            *self.bubbles[i, :2],
            labels[i],
            horizontalalignment="center",
            verticalalignment="center",
            fontfamily=self.font_family,
        )

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

Save the figure as a file.

Parameters:

Name Type Description Default
path Path | str

The path to the file to save.

required
**kwargs Any

Additional keyword arguments for plt.savefig.

{}
Source code in lexos/visualization/bubbleviz.py
@validate_call(config=model_config)
def save(self, path: Path | str, **kwargs: Any):
    """Save the figure as a file.

    Args:
        path (Path | str): The path to the file to save.
        **kwargs (Any): Additional keyword arguments for `plt.savefig`.
    """
    if path == "":
        raise LexosException("You must provide a valid path.")
    if self.fig is None:
        raise LexosException("The figure has not yet been generated.")
    self.fig.savefig(path, **kwargs)

show() ¤

Show the figure if it is hidden.

This is a helper method. You can also reference the figure using BubbleChart.fig. This will generally display in a Jupyter notebook.

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

    This is a helper method. You can also reference the figure using
    `BubbleChart.fig`. This will generally display in a Jupyter notebook.
    """
    return self.fig

D3 Bubble Visualizations¤

D3BubbleChart pydantic-model ¤

Bases: BaseModel

Class to render a D3 bubble chart visualization in HTML format.

Config:

  • arbitrary_types_allowed: True

Fields:

Source code in lexos/visualization/d3_bubbleviz.py
class D3BubbleChart(BaseModel):
    """Class to render a D3 bubble chart visualization in HTML format."""

    data: single_doc_types | multi_doc_types | pd.DataFrame = Field(
        ...,
        description="The data to generate the bubble chart 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."
    )
    title: Optional[str] = Field(
        "Bubble Chart Visualization", description="The title of the chart."
    )
    limit: Optional[int] = Field(
        None, description="The maximum number of bubbles to display."
    )
    height: int = Field(600, description="The height of the chart.")
    width: int = Field(960, description="The width of the chart.")
    margin: dict[str, int] = Field(
        {"top": 20, "right": 20, "bottom": 20, "left": 20},
        description="The margin around the chart.",
    )
    color: str | list[str] = Field(
        "schemeCategory10",
        description="The color scheme for the chart, either the name D3 color scheme or a list of custom colors.",
    )
    template: Path | str = Field(
        "d3_bubbles_template-1.0.html",
        description="The template file for the bubble chart visualization.",
    )
    auto_open: bool = Field(
        True, description="Whether to open the chart in a web browser automatically."
    )
    include_d3js: bool = Field(
        False, description="Whether to include the D3.js library in the HTML."
    )
    counts: dict[str, int] = Field(None, description="A dictionary of term counts.")
    html: str = Field(None, description="The rendered HTML for the bubble chart.")

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def __init__(self, **data):
        """Initialize the D3BubbleChart with the provided data."""
        super().__init__(**data)
        self.template = self._get_asset_path(self.template)
        # Process the data into a consistent format
        self.counts = processors.process_data(self.data, self.docs, self.limit)
        self._render()

    def _get_asset_path(self, filename: str) -> Path:
        """Centralized asset path resolution.

        Args:
            filename (str): The name of the asset file.

        Returns:
            Path: The full path to the asset file.
        """
        return Path(__file__).parent / "d3_cloud_assets" / filename

    def _load_template(self) -> str:
        """Load the HTML template for the bubble chart."""
        template_path = Path(self.template)
        try:
            return _load_local_asset(template_path)
        except FileNotFoundError:
            raise LexosException(f"Template file not found: {self.template}")

    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:
        """Render the bubble chart as an HTML string."""
        # Load the template
        template = Template(self._load_template())

        # Configure D3.js inclusion
        template.globals["include_d3js"] = self.include_d3js
        d3_js_script = ""
        if self.include_d3js:
            d3_js_script = _load_local_asset(self._get_asset_path("d3.v7.min.js"))

        # Render the template with the instance variables
        self.html = template.render(
            d3_js_script=d3_js_script,
            title=self.title,
            term_counts=self.counts,
            height=self.height,
            width=self.width,
            margin=self.margin,
            color=self.color,
        )

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

    @validate_call
    def save(self, path: Path | str) -> None:
        """Save the HTML file.

        Args:
            path (Path | str): The path where the HTML file will be saved.
        """
        with open(path, "w") as f:
            f.write(self.html)

auto_open: bool = True pydantic-field ¤

Whether to open the chart in a web browser automatically.

color: str | list[str] = 'schemeCategory10' pydantic-field ¤

The color scheme for the chart, either the name D3 color scheme or a list of custom colors.

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

The data to generate the bubble chart 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.

height: int = 600 pydantic-field ¤

The height of the chart.

html: str = None pydantic-field ¤

The rendered HTML for the bubble chart.

include_d3js: bool = False pydantic-field ¤

Whether to include the D3.js library in the HTML.

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

The maximum number of bubbles to display.

margin: dict[str, int] = {'top': 20, 'right': 20, 'bottom': 20, 'left': 20} pydantic-field ¤

The margin around the chart.

title: Optional[str] = 'Bubble Chart Visualization' pydantic-field ¤

The title of the chart.

width: int = 960 pydantic-field ¤

The width of the chart.

__init__(**data) ¤

Initialize the D3BubbleChart with the provided data.

Source code in lexos/visualization/d3_bubbleviz.py
def __init__(self, **data):
    """Initialize the D3BubbleChart with the provided data."""
    super().__init__(**data)
    self.template = self._get_asset_path(self.template)
    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)
    self._render()

save(path: Path | str) -> None ¤

Save the HTML file.

Parameters:

Name Type Description Default
path Path | str

The path where the HTML file will be saved.

required
Source code in lexos/visualization/d3_bubbleviz.py
@validate_call
def save(self, path: Path | str) -> None:
    """Save the HTML file.

    Args:
        path (Path | str): The path where the HTML file will be saved.
    """
    with open(path, "w") as f:
        f.write(self.html)

__init__(**data) ¤

Initialize the D3BubbleChart with the provided data.

Source code in lexos/visualization/d3_bubbleviz.py
def __init__(self, **data):
    """Initialize the D3BubbleChart with the provided data."""
    super().__init__(**data)
    self.template = self._get_asset_path(self.template)
    # Process the data into a consistent format
    self.counts = processors.process_data(self.data, self.docs, self.limit)
    self._render()

_get_asset_path(filename: str) -> Path ¤

Centralized asset path resolution.

Parameters:

Name Type Description Default
filename str

The name of the asset file.

required

Returns:

Name Type Description
Path Path

The full path to the asset file.

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

    Args:
        filename (str): The name of the asset file.

    Returns:
        Path: The full path to the asset file.
    """
    return Path(__file__).parent / "d3_cloud_assets" / filename

_load_template() -> str ¤

Load the HTML template for the bubble chart.

Source code in lexos/visualization/d3_bubbleviz.py
def _load_template(self) -> str:
    """Load the HTML template for the bubble chart."""
    template_path = Path(self.template)
    try:
        return _load_local_asset(template_path)
    except FileNotFoundError:
        raise LexosException(f"Template file not found: {self.template}")

_open() -> None ¤

Open the HTML file in a web browser.

Source code in lexos/visualization/d3_bubbleviz.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 ¤

Render the bubble chart as an HTML string.

Source code in lexos/visualization/d3_bubbleviz.py
def _render(self) -> None:
    """Render the bubble chart as an HTML string."""
    # Load the template
    template = Template(self._load_template())

    # Configure D3.js inclusion
    template.globals["include_d3js"] = self.include_d3js
    d3_js_script = ""
    if self.include_d3js:
        d3_js_script = _load_local_asset(self._get_asset_path("d3.v7.min.js"))

    # Render the template with the instance variables
    self.html = template.render(
        d3_js_script=d3_js_script,
        title=self.title,
        term_counts=self.counts,
        height=self.height,
        width=self.width,
        margin=self.margin,
        color=self.color,
    )

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

save(path: Path | str) -> None ¤

Save the HTML file.

Parameters:

Name Type Description Default
path Path | str

The path where the HTML file will be saved.

required
Source code in lexos/visualization/d3_bubbleviz.py
@validate_call
def save(self, path: Path | str) -> None:
    """Save the HTML file.

    Args:
        path (Path | str): The path where the HTML file will be saved.
    """
    with open(path, "w") as f:
        f.write(self.html)