Skip to content

Bubbleviz¤

The Bubbleviz module produces bubble charts, a variant on word clouds, that produces circles ("bubbles") for each term based on the term's frequency in a text or collection of texts.

lexos.visualization.bubbleviz.BubbleChart ¤

Bubble chart.

Source code in lexos\visualization\bubbleviz\__init__.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
class BubbleChart:
    """Bubble chart."""

    def __init__(self, BubbleChartModel: BubbleChartModel):
        """Instantiate a bubble chart from a BubbleChartModel.

        Args:
            BubbleChartModel (BubbleChartModel): A BubbleChartModel

        Notes:
            - If "area" is 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.
        """
        self.model = BubbleChartModel
        # Reduce the area to the limited number of terms
        area = np.asarray(self.model.area[: self.model.limit])
        r = np.sqrt(area / np.pi)

        self.bubble_spacing = self.model.bubble_spacing
        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()

    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 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 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 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 length of the distance between bubbles.
        """
        distance = self.outline_distance(bubble, bubbles)
        return len(distance[distance < 0])

    def collides_with(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
        """Collide.

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

        Returns:
            int: The minimum index.
        """
        distance = self.outline_distance(bubble, bubbles)
        idx_min = np.argmin(distance)
        return idx_min if type(idx_min) == np.ndarray else [idx_min]

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

        Args:
            n_iterations (int): Number of moves to perform.
        """
        for _i in range(n_iterations):
            moves = 0
            for i in range(len(self.bubbles)):
                rest_bub = np.delete(self.bubbles, i, 0)
                # Try to move directly towards the center of mass
                # Direction vector from bubble to the center of mass
                dir_vec = self.com - self.bubbles[i, :2]

                # Shorten direction vector to have length of 1
                dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))

                # 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])

                # Check whether new bubble collides with other bubbles
                if not self.check_collisions(new_bubble, rest_bub):
                    self.bubbles[i, :] = new_bubble
                    self.com = self.center_of_mass()
                    moves += 1
                else:
                    # Try to move around a bubble that you collide with
                    # Find colliding bubble
                    for colliding in self.collides_with(new_bubble, rest_bub):
                        # Calculate direction vector
                        dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
                        dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
                        # Calculate orthogonal vector
                        orth = np.array([dir_vec[1], -dir_vec[0]])
                        # test which direction to go
                        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 not self.check_collisions(new_bubble, rest_bub):
                            self.bubbles[i, :] = new_bubble
                            self.com = self.center_of_mass()

            if moves / len(self.bubbles) < 0.1:
                self.step_dist = self.step_dist / 2

    def plot(
        self,
        ax: object,
        labels: List[str],
        colors: List[str],
        font_family: str = "Arial",
    ):
        """Draw the bubble plot.

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

__init__(BubbleChartModel) ¤

Instantiate a bubble chart from a BubbleChartModel.

Parameters:

Name Type Description Default
BubbleChartModel BubbleChartModel

A BubbleChartModel

required
Notes
Source code in lexos\visualization\bubbleviz\__init__.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def __init__(self, BubbleChartModel: BubbleChartModel):
    """Instantiate a bubble chart from a BubbleChartModel.

    Args:
        BubbleChartModel (BubbleChartModel): A BubbleChartModel

    Notes:
        - If "area" is 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.
    """
    self.model = BubbleChartModel
    # Reduce the area to the limited number of terms
    area = np.asarray(self.model.area[: self.model.limit])
    r = np.sqrt(area / np.pi)

    self.bubble_spacing = self.model.bubble_spacing
    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()

center_distance(bubble, bubbles) ¤

Centre distance.

Parameters:

Name Type Description Default
bubble np.ndarray

Bubble array.

required
bubbles np.ndarray

Bubble array.

required

Returns:

Type Description
np.ndarray

np.ndarray: The centre distance.

Source code in lexos\visualization\bubbleviz\__init__.py
109
110
111
112
113
114
115
116
117
118
119
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() ¤

Centre of mass.

Returns:

Name Type Description
int int

The centre of mass.

Source code in lexos\visualization\bubbleviz\__init__.py
101
102
103
104
105
106
107
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, bubbles) ¤

Check collisions.

Parameters:

Name Type Description Default
bubble np.ndarray

Bubble array.

required
bubbles np.ndarray

Bubble array.

required

Returns:

Name Type Description
int int

The length of the distance between bubbles.

Source code in lexos\visualization\bubbleviz\__init__.py
134
135
136
137
138
139
140
141
142
143
144
145
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 length of the distance between bubbles.
    """
    distance = self.outline_distance(bubble, bubbles)
    return len(distance[distance < 0])

collapse(n_iterations=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\__init__.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def collapse(self, n_iterations: int = 50):
    """Move bubbles to the center of mass.

    Args:
        n_iterations (int): Number of moves to perform.
    """
    for _i in range(n_iterations):
        moves = 0
        for i in range(len(self.bubbles)):
            rest_bub = np.delete(self.bubbles, i, 0)
            # Try to move directly towards the center of mass
            # Direction vector from bubble to the center of mass
            dir_vec = self.com - self.bubbles[i, :2]

            # Shorten direction vector to have length of 1
            dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))

            # 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])

            # Check whether new bubble collides with other bubbles
            if not self.check_collisions(new_bubble, rest_bub):
                self.bubbles[i, :] = new_bubble
                self.com = self.center_of_mass()
                moves += 1
            else:
                # Try to move around a bubble that you collide with
                # Find colliding bubble
                for colliding in self.collides_with(new_bubble, rest_bub):
                    # Calculate direction vector
                    dir_vec = rest_bub[colliding, :2] - self.bubbles[i, :2]
                    dir_vec = dir_vec / np.sqrt(dir_vec.dot(dir_vec))
                    # Calculate orthogonal vector
                    orth = np.array([dir_vec[1], -dir_vec[0]])
                    # test which direction to go
                    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 not self.check_collisions(new_bubble, rest_bub):
                        self.bubbles[i, :] = new_bubble
                        self.com = self.center_of_mass()

        if moves / len(self.bubbles) < 0.1:
            self.step_dist = self.step_dist / 2

collides_with(bubble, bubbles) ¤

Collide.

Parameters:

Name Type Description Default
bubble np.ndarray

Bubble array.

required
bubbles np.ndarray

Bubble array.

required

Returns:

Name Type Description
int int

The minimum index.

Source code in lexos\visualization\bubbleviz\__init__.py
147
148
149
150
151
152
153
154
155
156
157
158
159
def collides_with(self, bubble: np.ndarray, bubbles: np.ndarray) -> int:
    """Collide.

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

    Returns:
        int: The minimum index.
    """
    distance = self.outline_distance(bubble, bubbles)
    idx_min = np.argmin(distance)
    return idx_min if type(idx_min) == np.ndarray else [idx_min]

outline_distance(bubble, bubbles) ¤

Outline distance.

Parameters:

Name Type Description Default
bubble np.ndarray

Bubble array.

required
bubbles np.ndarray

Bubble array.

required

Returns:

Name Type Description
int int

The outline distance.

Source code in lexos\visualization\bubbleviz\__init__.py
121
122
123
124
125
126
127
128
129
130
131
132
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, labels, colors, font_family='Arial') ¤

Draw the bubble plot.

Parameters:

Name Type Description Default
ax matplotlib.axes.Axes

The matplotlib axes.

required
labels List[str]

The labels of the bubbles.

required
colors List[str]

The colors of the bubbles.

required
font_family str

The font family.

'Arial'
Source code in lexos\visualization\bubbleviz\__init__.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def plot(
    self,
    ax: object,
    labels: List[str],
    colors: List[str],
    font_family: str = "Arial",
):
    """Draw the bubble plot.

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

lexos.visualization.bubbleviz.BubbleChartModel ¤

Bases: BaseModel

Ensure BubbleChart inputs are valid.

Source code in lexos\visualization\bubbleviz\__init__.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class BubbleChartModel(BaseModel):
    """Ensure BubbleChart inputs are valid."""

    terms: list
    area: list
    limit: Optional[int] = 100
    title: Optional[str] = None
    bubble_spacing: Optional[Union[float, int]] = 0.1
    colors: Optional[List[str]] = [
        "#5A69AF",
        "#579E65",
        "#F9C784",
        "#FC944A",
        "#F24C00",
        "#00B825",
    ]
    figsize: Optional[tuple] = (15, 15)
    font_family: Optional[str] = "DejaVu Sans"
    show: Optional[bool] = True
    filename: Optional[str] = None

    @validator("terms")
    def check_terms_not_empty(cls, v):
        """Ensure `terms` is not empty."""
        if v == []:
            raise ValueError("Empty term lists are not allowed.")
        return v

    @validator("area")
    def check_area_not_empty(cls, v):
        """Ensure `area` is not empty."""
        if v == []:
            raise ValueError("Empty area lists are not allowed.")
        return v

check_area_not_empty(v) ¤

Ensure area is not empty.

Source code in lexos\visualization\bubbleviz\__init__.py
49
50
51
52
53
54
@validator("area")
def check_area_not_empty(cls, v):
    """Ensure `area` is not empty."""
    if v == []:
        raise ValueError("Empty area lists are not allowed.")
    return v

check_terms_not_empty(v) ¤

Ensure terms is not empty.

Source code in lexos\visualization\bubbleviz\__init__.py
42
43
44
45
46
47
@validator("terms")
def check_terms_not_empty(cls, v):
    """Ensure `terms` is not empty."""
    if v == []:
        raise ValueError("Empty term lists are not allowed.")
    return v