0

I am using python-3.x and I would like to find a way to stop legend in the right of the line overlapping as you can see from the following image:

enter image description here

I am trying to make it look similar to the following image: -(note this figure was modified using image editor just to clarify what I want)

enter image description here

I tried many ways but none of them works for my case such as annotate. How I can stop legend text overlapping in matplotlib in my case?

This the code I am using: (all the values used are just an example)

data_1 = np.array([[0, 5, 3, 2 , 4, 7.7], [1, 1.5, 9, 7 , 8, 8], [2, 3, 3, 7 , 3, 3], [0, 5, 6, 12,4 , 3],[3, 5, 6, 10 ,2 , 6]])


df = pd.DataFrame({'111': data_1[0], '222': data_1[1], '333': data_1[2], '444': data_1[3], '555': data_1[4]})
# Graphing
#df.plot()
# 1. The color is a nice red / blue / green which is different from the primary color RGB
c = plt.get_cmap('Set1').colors
plt.rcParams['axes.prop_cycle'] = cycler(color = c)

fig, ax = plt.subplots(figsize = (7, 5))

# 2. Remove the legend
# 3. Make the line width thicker
df.plot(ax = ax, linewidth = 3, legend = False)

# 4. Display y-axis label
# 5. Change the display range of x-axis and y-axis
x_min, x_max = 0, 5
y_min, y_max = 0, 13
ax.set(ylim = (y_min, y_max), xlim = (x_min, x_max + 0.03))

# 6. Specify font size collectively
plt.rcParams["font.size"] = 14

# 7. Display graph title, X axis, Y axis name (label), grid line
plt.title("title")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)

# 8. Remove the right and top frame
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(True)

# 9. Show index to the right of the plot instead of the normal legend
for i, name in enumerate(df.columns.values):
    ax.text(x_max + 0.03, ax.lines[i].get_data()[1][-1], name, color = f'C{i}', va = 'center')

plt.savefig('plot_lines.png', dpi = 300  ,bbox_inches = 'tight')

plt.show() 

Any ideas?

azeez
  • 469
  • 3
  • 6
  • 17
  • Matplotlib has no built-in way to avoid overlaps of text labels. You may of course calculate the coordinates manually. There is also a package called `adjustText`. [Here](https://stackoverflow.com/questions/19073683/matplotlib-overlapping-annotations-text) are your options. – ImportanceOfBeingErnest Jun 10 '19 at 21:59

2 Answers2

1

The answer to this question cab be adapted to your case:

spread_labels

The idea is to generate a graph with as many nodes as you have data points and related label points. You only want the label nodes to spread out. You can do so with network.spring_layout() graph (see documentation here).

The attached code implements a function spring_labels() for clarity. Arguments of interests are

  • hint (array-like): some data points have exactly the same y coordinate, so the graph will spread the related labels as the same position. You can give a different value to each data point with the hint keyword argument in order to increase differentiability (here I took the second to last point)
  • spread (float): this controls how far the nodes are spread; this value should be set manually in order to optimize the results
  • shift (float): the shift in x direction of the label x-coordinate.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import networkx as nx

from cycler import cycler

def spring_labels(ax, x, y, labels, spread=.03, shift=.1, hint=None, colors=None):

    if hint is None:
        hint = y

    if colors is None:
        colors = ['C{}' for i in range(len(y))]

    # Create graph
    graph = nx.DiGraph()
    # node_labels = labels
    node_data = ['data_{}'.format(l) for l in labels]
    graph.add_nodes_from(node_data + labels)
    graph.add_edges_from(zip(node_data, labels))

    # Initialize position
    graph_init = dict()
    for yi, yh, nd, nl in zip(y, hint, node_data, labels):
        graph_init[nd] = (x, yi)
        graph_init[nl] = (x + shift, yi + (yi - yh) * .1)

    # Draw spring-force graph
    positions = nx.spring_layout(graph, pos=graph_init, fixed=node_data, k=spread)
    for label in labels:
        positions[label][0] = x + shift

    # colors = plt.rcParams['axes.color_cycle']
    # print(colors)
    for (data, label), color in zip(graph.edges, colors):
        ax.plot([positions[label][0], positions[data][0]],
                [positions[label][1], positions[data][1]],
                color=color, clip_on=False)
        ax.text(*positions[label], label, color=color)

data_1 = np.array([[0, 5, 3, 2, 4, 7.7], [1, 1.5, 9, 7, 8, 8], [
                  2, 3, 3, 7, 3, 3], [0, 5, 6, 12, 4, 3], [3, 5, 6, 10, 2, 6]])

df = pd.DataFrame({'111': data_1[0], '222': data_1[1], '333': data_1[
                  2], '444': data_1[3], '555': data_1[4]})
# Graphing
# df.plot()
# 1. The color is a nice red / blue / green which is different from the
# primary color RGB
c = plt.get_cmap('Set1').colors
plt.rcParams['axes.prop_cycle'] = cycler(color=c)

fig, ax = plt.subplots(figsize=(7, 5))

# 2. Remove the legend
# 3. Make the line width thicker
df.plot(ax=ax, linewidth=3, legend=False)

# 4. Display y-axis label
# 5. Change the display range of x-axis and y-axis
x_min, x_max = 0, 5
y_min, y_max = 0, 13
ax.set(ylim=(y_min, y_max), xlim=(x_min, x_max + 0.03))

# 6. Specify font size collectively
plt.rcParams["font.size"] = 14

# 7. Display graph title, X axis, Y axis name (label), grid line
plt.title("title")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)

# 8. Remove the right and top frame
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(True)

# 9. Show index to the right of the plot instead of the normal legend
ys_hint = [a.get_data()[1][-2] for a in ax.lines]
ys_max = [a.get_data()[1][-1] for a in ax.lines]
spring_labels(ax, x_max, ys_max, df.columns.values, shift=.2, hint=ys_hint, colors=c)

plt.savefig('plot_lines.png', dpi=300, bbox_inches='tight')
Leonard
  • 2,510
  • 18
  • 37
0

This might be considered the worst hack in matplotlib history but at least for your current example, this is something I just came up with briefly. This is not perfect yet and I would be more than happy to delete it, if it is of no use. But since I gave some time to get so far, I thought of sharing it with you.

The idea is to sort the y-values and then use a rescaling factor adding an offset where the offset is manually set to 0 for the index of orange curve which is 2 (third value in increasing order) and that's why (i-2)

yvals = np.array([ax.lines[i].get_data()[1][-1] for i in range(len(df.columns.values))])
indexs = np.argsort(yvals)
values = df.columns.values

for i, idx in enumerate(indexs):
    print (yvals[idx]+0.5*i)
    ax.text(x_max + 0.1, yvals[idx]+0.45*(i-2), name, color = f'C{idx}', va = 'center')

enter image description here

Sheldore
  • 37,862
  • 7
  • 57
  • 71