Search code examples
pythonstringstring-length

How to specify spacings among words of several sentences to force nth word in each sentence start from exact the same coordinate on plot in python


I am trying to write some grouped texts, each sentence in each group contain 4 parts: value + unit + symbol + value e.g., 0.1 (psi) -> 0.0223, on a plot. Each group will begin from a specified coordinate, but I couldn't force the second parts (units) to begin from an exact the same coordinate as each other in each group. Now, I am using a calculated value * " " after the first parts to force the second parts start from the same point, where the calculated value is determined based on the number of letters, not a metric feature, of the first parts. For this, firstly, I find the longest value of the first part in each group, then the length of that (maximum length), then for each value (the first part) in that group the length of that value + (maximum length - the length of that value) * " ", but they will be appeared irregular (shown on the pic) in some cases, which, I think, might be due to different width of digits in each value e.g., 0 is a little wider than 1. Is there any way to cure it, perhaps something like a metric feature (not based on the number of letters) or something that force each digit or letter to occupy a specific width? How?

import numpy as np
import matplotlib.pyplot as plt

# data ----------------------------------
data = {"Dev":  [0, 30, 60], "Bor":  [1.750, 2.875, 4.125, 6.125, 8.500, 12.250],
        "Poi":  [0, 0.1, 0.2, 0.3, 0.4, 0.5], "Str":  [0, 0.33, 0.5, 1]}
units = [["(deg)", "(in)"], ["(unitless)"], ["(psi)"]]
Inputs = list(data.values())
area_ratio = [[0.16734375, 0.043875, 0.0], [1.0, 0.93, 0.67886875, 0.3375, 0.16158125, 0.0664125],
              [0.26145, 0.23625, 0.209475, 0.1827, 0.155925, 0.12915], [0.451484375, 0.163359375, 0.106984375, 0.05253125]]
x_bar_poss = [np.array([3.7, 4., 4.3]), np.array([5.25, 5.55, 5.85, 6.15, 6.45, 6.75]),
              np.array([9.25,  9.55,  9.85, 10.15, 10.45, 10.75]), np.array([13.55, 13.85, 14.15, 14.45])]

colors = ['green', 'orange', 'purple', 'yellow', 'gray', 'olive']
units_ravel = [item for sublist in units for item in sublist]

# code ----------------------------------


def max_string_len(list_):
    max_len = 0
    for i in list_:
        max_len = max(len(str(i)), max_len)
    return max_len


fig, ax = plt.subplots()
for i, row in enumerate(area_ratio):
    max_hight = max(row)
    max_str_len = max_string_len(Inputs[i])
    for j, k in enumerate(row):
        plt.bar(x_bar_poss[i][j], k, width=0.3, color=colors[j], edgecolor='black')
        # ==============================================================================================================
        plt_text = str(Inputs[i][j]) + (max_str_len - len(str(Inputs[i][j])) + 1) * " " + units_ravel[i] \
                   + r"$\longmapsto$" + f'{area_ratio[i][j]:.5f}'
        # ==============================================================================================================
        plt.text(x_bar_poss[i][j], 0.75, plt_text, rotation=90, ha='center', va='bottom')

ax.set(xlim=(0, 16), ylim=(0, 1), yticks=np.linspace(0, 1, 6))
plt.show()

enter image description here


Solution

  • I found a solution for that and posting it in a raw format just for the prepared example, which can be improved for other conditions. It can be achieved by separating the sentence into two parts. The first part will be start from a specified y-axis value. By placing the first parts on a temporary window, maximum length of them in inch and maximum character length for each group will be determined. For space between the two words, length of each character in inch will be estimated by dividing maximum length in inch with maximum character length. By adding this to the length of the first part in inch, the total length can be converted to the corresponding length on y-axis (not in inch). So, the starting y-axis value for the second part will be (max_len + max_len / max_char_len) / y_inch * y_lim[1] (note that y_lim[1] is used because y-axis started from 0, otherwise y_lim[1] - y_lim[0]).

    fig, ax = plt.subplots()
    y_lim = ax.get_ylim()
    y_inch = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()).height
    
    h = []
    for row in data.values():
        max_len = 0
        max_char_len = 0
        for val in row:
            text_obj = ax.text(0, 0, val)
            bbox = text_obj.get_window_extent(renderer=ax.figure.canvas.get_renderer()).transformed(fig.dpi_scale_trans.inverted())
            text_obj.remove()
    
            max_len = max(bbox.width, max_len)
            max_char_len = max(len(str(val)), max_char_len)
        h.append((max_len + max_len / max_char_len) / y_inch * y_lim[1])
    
    for i, row in enumerate(area_ratio):
        max_hight = max(row)
        max_str_len = max_string_len(Inputs[i])
        for j, k in enumerate(row):
            plt.bar(x_bar_poss[i][j], k, width=0.3, color=colors[j], edgecolor='black')
            second_part = units_ravel[i] + r"$\longmapsto$" + f'{area_ratio[i][j]:.5f}'
            h1 = 0.75
            h2 = h1 + h[i]
            plt.text(x_bar_poss[i][j], h1, str(Inputs[i][j]), rotation=90, ha='center', va='bottom')
            plt.text(x_bar_poss[i][j], h2, second_part, rotation=90, ha='center', va='bottom')
    
    ax.set(xlim=(0, 16), ylim=(0, 1), yticks=np.linspace(0, 1, 6))
    plt.show()
    

    enter image description here