matplotlib 绘制圆并将其放大,以便内部的文本不会超出圆的边界

gmol1639  于 2023-11-22  发布在  其他
关注(0)|答案(1)|浏览(100)

我有一些数据,我有语言和相对单位大小。我想产生一个气泡图,然后将其导出到PGF。我得到了我的大部分代码从这个答案Making a non-overlapping bubble chart in Matplotlib (circle packing),但我有问题,我的文本退出圆边界:

我该怎么做呢?增加所有内容的规模(我认为这很容易),或者确保气泡的大小总是大于里面的文本(根据数据序列,气泡之间仍然是成比例的)。我认为这很难做到,但我真的不需要。
相关编码:

#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

# create 10 circles with different radii
r = np.random.randint(5,15, size=10)
mapping = [("English", 25),
                    ("French", 13),
                    ("Spanish", 32),
                    ("Thai", 10),
                    ("Vientamese", 13),
                    ("Chinese", 20),
                    ("Jamaican", 8),
                    ("Scottish", 3),
                    ("Irish", 12),
                    ("American", 5),
                    ("Romanian", 3),
                    ("Dutch", 2)]

class C():
    def __init__(self,r):
        self.colors = list(mcolors.XKCD_COLORS)
        self.N = len(r)
        self.labels = [item[0] for item in r]
        self.x = np.ones((self.N,3))
        self.x[:,2] = [item[1] for item in r]
        maxstep = 2*self.x[:,2].max()
        length = np.ceil(np.sqrt(self.N))
        grid = np.arange(0,length*maxstep,maxstep)
        gx,gy = np.meshgrid(grid,grid)
        self.x[:,0] = gx.flatten()[:self.N]
        self.x[:,1] = gy.flatten()[:self.N]
        self.x[:,:2] = self.x[:,:2] - np.mean(self.x[:,:2], axis=0)

        self.step = self.x[:,2].min()
        self.p = lambda x,y: np.sum((x**2+y**2)**2)
        self.E = self.energy()
        self.iter = 1.

    def minimize(self):
        while self.iter < 1000*self.N:
            for i in range(self.N):
                rand = np.random.randn(2)*self.step/self.iter
                self.x[i,:2] += rand
                e = self.energy()
                if (e < self.E and self.isvalid(i)):
                    self.E = e
                    self.iter = 1.
                else:
                    self.x[i,:2] -= rand
                    self.iter += 1.

    def energy(self):
        return self.p(self.x[:,0], self.x[:,1])

    def distance(self,x1,x2):
        return np.sqrt((x1[0]-x2[0])**2+(x1[1]-x2[1])**2)-x1[2]-x2[2]

    def isvalid(self, i):
        for j in range(self.N):
            if i!=j:
                if self.distance(self.x[i,:], self.x[j,:]) < 0:
                    return False
        return True

    def scale(self, size):
        """Scales up the plot"""
        self.x = self.x*size

    def plot(self, ax):
        for i in range(self.N):
            circ = plt.Circle(self.x[i,:2],self.x[i,2], color=mcolors.XKCD_COLORS[self.colors[i]])
            ax.add_patch(circ)
            ax.text(self.x[i][0],self.x[i][1], self.labels[i], horizontalalignment='center', size='medium', color='black', weight='semibold')

c = C(mapping)

fig, ax = plt.subplots(subplot_kw=dict(aspect="equal"))
ax.axis("off")

c.minimize()

c.plot(ax)
ax.relim()
ax.autoscale_view()
plt.show()

字符串

ruyhziif

ruyhziif1#

我认为你概述的这两种方法在很大程度上是等效的。在这两种情况下,你必须根据圆圈的大小来确定文本框的大小。为matplotlib文本对象获取精确的边界框是一件棘手的事情,因为渲染文本对象是由后端完成的,而不是matplotlib本身。所以你必须渲染文本对象,获取其边界框,计算当前边界和所需边界之间的比率,移除文本对象,最后重新渲染按先前计算的比率重新缩放的文本。并且由于边界框计算以及因此重新缩放对于非常小和非常大的文本对象是非常不准确的,你实际上必须重复这个过程几次(下面我做了两次,这是最低限度的)。
关于圆圈的位置,我也冒昧地用适当的最小化来代替你在能量景观中的随机漫步,它更快,我认为结果更好。


的数据

#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

from scipy.optimize import minimize, NonlinearConstraint
from scipy.spatial.distance import pdist, squareform

def _get_fontsize(size, label, ax, *args, **kwargs):
    """Given a circle, precompute the fontsize for a text object such that it fits the circle snuggly.

    Parameters
    ----------
    size : float
        The radius of the circle.
    label : str
        The string.
    ax : matplotlib.axis object
        The matplotlib axis.
    *args, **kwargs
        Passed to ax.text().

    Returns
    -------
    fontsize : float
        The estimated fontsize.
    """

    default_fontsize = kwargs.setdefault('size', plt.rcParams['font.size'])
    width, height = _get_text_object_dimensions(ax, label, *args, **kwargs)
    initial_estimate = size / (np.sqrt(width**2 + height**2) / 2) * default_fontsize
    kwargs['size'] = initial_estimate
    # Repeat process as bbox estimates are bad for very small and very large bboxes.
    width, height = _get_text_object_dimensions(ax, label, *args, **kwargs)
    return size / (np.sqrt(width**2 + height**2) / 2) * initial_estimate

def _get_text_object_dimensions(ax, string, *args, **kwargs):
    """Precompute the dimensions of a text object on a given axis in data coordinates.

    Parameters
    ----------
    ax : matplotlib.axis object
        The matplotlib axis.
    string : str
        The string.
    *args, **kwargs
        Passed to ax.text().

    Returns
    -------
    width, height : float
        The dimensions of the text box in data units.
    """

    text_object = ax.text(0., 0., string, *args, **kwargs)
    renderer = _find_renderer(text_object.get_figure())
    bbox_in_display_coordinates = text_object.get_window_extent(renderer)
    bbox_in_data_coordinates = bbox_in_display_coordinates.transformed(ax.transData.inverted())
    w, h = bbox_in_data_coordinates.width, bbox_in_data_coordinates.height
    text_object.remove()
    return w, h

def _find_renderer(fig):
    """
    Return the renderer for a given matplotlib figure.

    Notes
    -----
    Adapted from https://stackoverflow.com/a/22689498/2912349
    """

    if hasattr(fig.canvas, "get_renderer"):
        # Some backends, such as TkAgg, have the get_renderer method, which
        # makes this easy.
        renderer = fig.canvas.get_renderer()
    else:
        # Other backends do not have the get_renderer method, so we have a work
        # around to find the renderer. Print the figure to a temporary file
        # object, and then grab the renderer that was used.
        # (I stole this trick from the matplotlib backend_bases.py
        # print_figure() method.)
        import io
        fig.canvas.print_pdf(io.BytesIO())
        renderer = fig._cachedRenderer
    return(renderer)

class BubbleChart:

    def __init__(self, sizes, colors, labels, ax=None, **font_kwargs):
        # TODO: input sanitation

        self.sizes = np.array(sizes)
        self.labels = labels
        self.colors = colors
        self.ax = ax if ax else plt.gca()

        self.positions = self._initialize_positions(self.sizes)
        self.positions = self._optimize_positions(self.positions, self.sizes)
        self._plot_bubbles(self.positions, self.sizes, self.colors, self.ax)

        # NB: axis limits have to be finalized before computing fontsizes
        self._rescale_axis(self.ax)

        self._plot_labels(self.positions, self.sizes, self.labels, self.ax, **font_kwargs)

    def _initialize_positions(self, sizes):
        # TODO: try different strategies; set initial positions to lie
        # - on a circle
        # - on concentric shells, larger bubbles on the outside
        return np.random.rand(len(sizes), 2) * np.min(sizes)

    def _optimize_positions(self, positions, sizes):
        # Adapted from: https://stackoverflow.com/a/73353731/2912349

        def cost_function(new_positions, old_positions):
            return np.sum((new_positions.reshape((-1, 2)) - old_positions)**2)

        def constraint_function(x):
            x = np.reshape(x, (-1, 2))
            return pdist(x)

        lower_bounds = sizes[np.newaxis, :] + sizes[:, np.newaxis]
        lower_bounds -= np.diag(np.diag(lower_bounds)) # squareform requires zeros on diagonal
        lower_bounds = squareform(lower_bounds)

        nonlinear_constraint = NonlinearConstraint(constraint_function, lower_bounds, np.inf, jac='2-point')
        result = minimize(lambda x: cost_function(x, positions), positions.flatten(), method='SLSQP',
                        jac="2-point", constraints=[nonlinear_constraint])
        return result.x.reshape((-1, 2))

    def _plot_bubbles(self, positions, sizes, colors, ax):
        for (x, y), radius, color in zip(positions, sizes, colors):
            ax.add_patch(plt.Circle((x, y), radius, color=color))

    def _rescale_axis(self, ax):
        ax.relim()
        ax.autoscale_view()
        ax.get_figure().canvas.draw()

    def _plot_labels(self, positions, sizes, labels, ax, **font_kwargs):
        font_kwargs.setdefault('horizontalalignment', 'center')
        font_kwargs.setdefault('verticalalignment', 'center')

        for (x, y), label, size in zip(positions, labels, sizes):
            fontsize = _get_fontsize(size, label, ax, **font_kwargs)
            ax.text(x, y, label, size=fontsize, **font_kwargs)

if __name__ == '__main__':

    mapping = [("English", 25),
               ("French", 13),
               ("Spanish", 32),
               ("Thai", 10),
               ("Vietnamese", 13),
               ("Chinese", 20),
               ("Jamaican", 8),
               ("Scottish", 3),
               ("Irish", 12),
               ("American", 5),
               ("Romanian", 3),
               ("Dutch", 2)]

    labels = [item[0] for item in mapping]
    sizes = [item[1] for item in mapping]
    colors = list(mcolors.XKCD_COLORS)

    fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(aspect="equal"))
    bc = BubbleChart(sizes, colors, labels, ax=ax)
    ax.axis("off")
    plt.show()

字符串

相关问题