Skip to content

Commit e777a49

Browse files
committed
Set tick positions and horizontal alignment
This patch allows to define on which axes ticks are displayed and to define the horizontal alignment of the labels. In addition, it is possible to pass the width of the labels to the save function as this value is required for pgfplots to work correctly. The value can be passed as the 'x tick label text width' and 'y tick label text width' in the extra dictionary. In addition new unit tests are provided.
1 parent 5f7fc6d commit e777a49

29 files changed

+399
-57
lines changed

matplotlib2tikz/axes.py

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -199,33 +199,27 @@ def __init__(self, data, obj):
199199
self.axis_options.append('tick align=center')
200200

201201
# Set each rotation for every label
202-
x_tick_labels_rotation = \
203-
[label.get_rotation() for label in obj.xaxis.get_majorticklabels()]
204-
if any(x_tick_labels_rotation) != 0:
205-
# check if every value is the same
206-
if len(set(x_tick_labels_rotation)) == 1:
207-
self.axis_options.append('xticklabel style = {rotate=%d}' %
208-
x_tick_labels_rotation[0])
209-
else:
210-
value = 'every x tick label/.style = {\n' \
211-
'rotate={,%s}[\\ticknum]\n' \
212-
'}' % ','.join(str(x) for x in x_tick_labels_rotation)
213-
214-
self.axis_options.append(value)
215-
216-
y_tick_labels_rotation = \
217-
[label.get_rotation() for label in obj.yaxis.get_majorticklabels()]
218-
if any(y_tick_labels_rotation) != 0:
219-
# check if every value is the same
220-
if len(set(y_tick_labels_rotation)) == 1:
221-
self.axis_options.append('yticklabel style = {rotate=%d}' %
222-
y_tick_labels_rotation[0])
223-
else:
224-
value = 'every y tick label/.style = {\n' \
225-
'rotate={,%s}[\\ticknum]\n' \
226-
'}' % ','.join(str(x) for x in y_tick_labels_rotation)
227-
228-
self.axis_options.append(value)
202+
x_tick_rotation_and_horizontal_alignment = \
203+
self.__get_label_rotation_and_horizontal_alignment(obj, data, 'x')
204+
if x_tick_rotation_and_horizontal_alignment:
205+
self.axis_options.append(x_tick_rotation_and_horizontal_alignment)
206+
207+
y_tick_rotation_and_horizontal_alignment = \
208+
self.__get_label_rotation_and_horizontal_alignment(obj, data, 'y')
209+
if y_tick_rotation_and_horizontal_alignment:
210+
self.axis_options.append(y_tick_rotation_and_horizontal_alignment)
211+
212+
# Set tick position
213+
x_tick_position_string, x_tick_position = \
214+
self.__get_tick_position(obj, data, 'x')
215+
y_tick_position_string, y_tick_position = \
216+
self.__get_tick_position(obj, data, 'y')
217+
218+
if x_tick_position == y_tick_position and x_tick_position is not None:
219+
self.axis_options.append("tick pos=%s" % x_tick_position)
220+
else:
221+
self.axis_options.append(x_tick_position_string)
222+
self.axis_options.append(y_tick_position_string)
229223

230224
# Don't use get_{x,y}gridlines for gridlines; see discussion on
231225
# <http://sourceforge.net/p/matplotlib/mailman/message/25169234/>
@@ -389,6 +383,113 @@ def __init__(self, data, obj):
389383

390384
return
391385

386+
def __get_label_rotation_and_horizontal_alignment(self, obj, data, axes):
387+
tick_label_text_width = None
388+
tick_label_text_width_identifier = "%s tick label text width" % axes
389+
if tick_label_text_width_identifier in data['extra axis options']:
390+
tick_label_text_width = data['extra axis options [base]']
391+
[tick_label_text_width_identifier]
392+
del data['extra axis options'][tick_label_text_width_identifier]
393+
394+
label_style = ""
395+
396+
major_tick_labels = obj.xaxis.get_majorticklabels() if axes == 'x' \
397+
else obj.yaxis.get_majorticklabels()
398+
399+
if len(major_tick_labels) == 0:
400+
return None
401+
402+
tick_labels_rotation = \
403+
[label.get_rotation() for label in major_tick_labels]
404+
tick_labels_rotation_same_value = len(set(tick_labels_rotation)) == 1
405+
406+
tick_labels_horizontal_alignment = \
407+
[label.get_horizontalalignment() for label in major_tick_labels]
408+
tick_labels_horizontal_alignment_same_value = \
409+
len(set(tick_labels_horizontal_alignment)) == 1
410+
411+
if tick_labels_rotation_same_value and \
412+
tick_labels_horizontal_alignment_same_value:
413+
values = []
414+
415+
if any(tick_labels_rotation) != 0:
416+
values.append('rotate=%d' % tick_labels_rotation[0])
417+
418+
if tick_label_text_width:
419+
values.append('align=%s' % tick_labels_horizontal_alignment[0])
420+
values.append('text width=%s' % tick_label_text_width)
421+
else:
422+
print('Horizontal alignment will be ignored as no \'%s tick '
423+
'label text width\' has been passed in the \'extra\' '
424+
'parameter' % axes)
425+
426+
if len(values) > 0:
427+
label_style = \
428+
'%sticklabel style = {%s}' % (axes, ','.join(values))
429+
else:
430+
values = []
431+
432+
if tick_labels_rotation_same_value:
433+
values.append('rotate=%d' % tick_labels_rotation[0])
434+
else:
435+
values.append('rotate={%s,0}[\\ticknum]'
436+
% ','.join(str(x) for x in tick_labels_rotation))
437+
438+
if tick_label_text_width:
439+
if tick_labels_horizontal_alignment_same_value:
440+
values.append('align=%s' %
441+
tick_labels_horizontal_alignment[0])
442+
values.append('text width=%s' % tick_label_text_width)
443+
else:
444+
for idx, x in enumerate(tick_labels_horizontal_alignment):
445+
label_style += '%s_tick_label_ha_%d/.initial = %s' \
446+
% (axes, idx, x)
447+
448+
values.append(
449+
'align=\pgfkeysvalueof{/pgfplots/'
450+
'%s_tick_label_ha_\\ticknum}' % axes)
451+
values.append('text width=%s' % tick_label_text_width)
452+
else:
453+
print('Horizontal alignment will be ignored as no \'%s tick '
454+
'label text width\' has been passed in the \'extra\' '
455+
'parameter' % axes)
456+
457+
label_style = 'every %s tick label/.style = {\n' \
458+
'%s\n' \
459+
'}' % (axes, ',\n'.join(values))
460+
461+
return label_style
462+
463+
def __get_tick_position(self, obj, data, axes):
464+
major_ticks = obj.xaxis.majorTicks if axes == 'x' else \
465+
obj.yaxis.majorTicks
466+
467+
major_ticks_bottom = [tick.tick1On for tick in major_ticks]
468+
major_ticks_top = [tick.tick2On for tick in major_ticks]
469+
470+
major_ticks_bottom_show_all = False
471+
if len(set(major_ticks_bottom)) == 1 and major_ticks_bottom[0] is True:
472+
major_ticks_bottom_show_all = True
473+
474+
major_ticks_top_show_all = False
475+
if len(set(major_ticks_top)) == 1 and major_ticks_top[0] is True:
476+
major_ticks_top_show_all = True
477+
478+
major_ticks_position = None
479+
if not major_ticks_bottom_show_all and not major_ticks_top_show_all:
480+
position_string = "%smajorticks=false" % axes
481+
elif major_ticks_bottom_show_all and major_ticks_top_show_all:
482+
major_ticks_position = 'both'
483+
elif major_ticks_bottom_show_all:
484+
major_ticks_position = 'left'
485+
elif major_ticks_top_show_all:
486+
major_ticks_position = 'right'
487+
488+
if major_ticks_position:
489+
position_string = "%stick pos=%s" % (axes, major_ticks_position)
490+
491+
return position_string, major_ticks_position
492+
392493
def get_begin_code(self):
393494
content = self.content
394495
if self.axis_options:

test/test_rotated_labels.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
import matplotlib2tikz
4+
5+
import pytest
6+
import tempfile
7+
import os
8+
from matplotlib import pyplot as plt
9+
10+
11+
def __plot():
12+
fig, ax = plt.subplots()
13+
14+
x = [1, 2, 3, 4]
15+
y = [1, 4, 9, 6]
16+
17+
plt.plot(x, y, 'ro')
18+
plt.xticks(x, rotation='horizontal')
19+
20+
return fig, ax
21+
22+
23+
@pytest.mark.parametrize(
24+
"x_alignment, y_alignment, x_tick_label_width,"
25+
"y_tick_label_width, rotation", [
26+
(None, None, "1rem", "3rem", 90),
27+
(None, "center", "1rem", "3rem", 90),
28+
("center", None, "1rem", "3rem", 90),
29+
("center", "center", None, "3rem", 90),
30+
("left", "left", None, "3rem", 90),
31+
("right", "right", None, "3rem", 90),
32+
("center", "center", "1rem", None, 90),
33+
("left", "left", "2rem", None, 90),
34+
("right", "right", "3rem", None, 90),
35+
("center", "center", "1rem", "3rem", 90),
36+
("left", "left", "2rem", "3rem", 90),
37+
("right", "right", "3rem", "3rem", 90),
38+
("left", "right", "2rem", "3rem", 90),
39+
("right", "left", "3rem", "3rem", 90),
40+
])
41+
def test_rotated_labels_parameters(x_alignment, y_alignment,
42+
x_tick_label_width, y_tick_label_width,
43+
rotation):
44+
fig, _ = __plot()
45+
46+
if x_alignment:
47+
plt.xticks(ha=x_alignment, rotation=rotation)
48+
if y_alignment:
49+
plt.yticks(ha=y_alignment, rotation=rotation)
50+
51+
# convert to tikz file
52+
_, tmp_base = tempfile.mkstemp()
53+
tikz_file = tmp_base + '_tikz.tex'
54+
55+
extra_dict = {}
56+
57+
if x_tick_label_width:
58+
extra_dict['x tick label text width'] = x_tick_label_width
59+
if y_tick_label_width:
60+
extra_dict['y tick label text width'] = y_tick_label_width
61+
62+
matplotlib2tikz.save(
63+
tikz_file,
64+
figurewidth='7.5cm',
65+
extra=extra_dict
66+
)
67+
68+
# close figure
69+
plt.close(fig)
70+
71+
# delete file
72+
os.unlink(tikz_file)
73+
74+
75+
@pytest.mark.parametrize("x_tick_label_width, y_tick_label_width", [
76+
(None, None),
77+
("1rem", None),
78+
(None, "3rem"),
79+
("2rem", "3rem"),
80+
])
81+
def test_rotated_labels_parameters_different_values(x_tick_label_width,
82+
y_tick_label_width):
83+
fig, ax = __plot()
84+
85+
plt.xticks(ha='left', rotation=90)
86+
plt.yticks(ha='left', rotation=90)
87+
ax.xaxis.get_majorticklabels()[0].set_rotation(20)
88+
ax.yaxis.get_majorticklabels()[0].set_horizontalalignment('right')
89+
90+
# convert to tikz file
91+
_, tmp_base = tempfile.mkstemp()
92+
tikz_file = tmp_base + '_tikz.tex'
93+
94+
extra_dict = {}
95+
96+
if x_tick_label_width:
97+
extra_dict['x tick label text width'] = x_tick_label_width
98+
if y_tick_label_width:
99+
extra_dict['y tick label text width'] = y_tick_label_width
100+
101+
matplotlib2tikz.save(
102+
tikz_file,
103+
figurewidth='7.5cm',
104+
extra=extra_dict
105+
)
106+
107+
# close figure
108+
plt.close(fig)
109+
110+
# delete file
111+
os.unlink(tikz_file)
112+
113+
114+
def test_rotated_labels_parameters_no_ticks():
115+
fig, ax = __plot()
116+
117+
ax.xaxis.set_ticks([])
118+
119+
plt.tick_params(axis='x',
120+
which='both',
121+
bottom='off',
122+
top='off')
123+
plt.tick_params(axis='y',
124+
which='both',
125+
left='off',
126+
right='off')
127+
128+
# convert to tikz file
129+
_, tmp_base = tempfile.mkstemp()
130+
tikz_file = tmp_base + '_tikz.tex'
131+
132+
matplotlib2tikz.save(
133+
tikz_file,
134+
figurewidth='7.5cm'
135+
)
136+
137+
# close figure
138+
plt.close(fig)
139+
140+
# delete file
141+
os.unlink(tikz_file)

test/testfunctions/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'patches',
2727
'quadmesh',
2828
'rotated_labels',
29+
'tick_positions',
2930
'scatter',
3031
'scatter_colormap',
3132
'sharex_and_y',

test/testfunctions/annotate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
desc = 'Annotations'
44
# phash = 'ab8a78a1549654fe'
5-
phash = 'ab8a71a1569654de'
5+
phash = 'ab8a79a1549654be'
66

77

88
def plot():

test/testfunctions/barchart.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
88
"""
99
desc = 'Bar Chart'
10-
phash = '5f09a9e6b3728742'
10+
phash = '5f09a9e6b172874a'
11+
1112

1213
def plot():
1314
import matplotlib.pyplot as plt

test/testfunctions/barchart_errorbars.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
77
"""
88
desc = 'Bar Chart With Errorbars'
9-
phash = '5f09a9e4b37287c2'
9+
phash = '5f09a9e6b1728746'
10+
1011

1112
def plot():
1213
import matplotlib.pyplot as plt

test/testfunctions/barchart_legend.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
patches that should not be plotted in PGFPlots (e.g. axis, legend)
77
88
This also tests legends on barcharts. Which are difficult because
9-
in PGFPlots, they have no \\addplot, and thus legend must be
9+
in PGFPlots, they have no \\addplot, and thus legend must be
1010
manually added.
1111
1212
"""
1313
desc = 'Bar Chart'
14-
phash = '5f09a9e633728dc4'
14+
phash = '5f19a966937285cc'
15+
1516

1617
def plot():
1718
import matplotlib.pyplot as plt

test/testfunctions/basic_sin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
desc = 'Simple $\sin$ plot with some labels'
44
# phash = '5f34e1ce21c3e5c1'
5-
phash = '1f36e1ce21c1e7c1'
5+
phash = '1f36e5c621c1e7c1'
66

77

88
def plot():

test/testfunctions/boxplot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
44
This test plots a box plot with three data series. The causes an empty Line2D
55
to be plotted. Without care, this can turn into an empty table in PGFPlot
6-
which crashes latex (due to it treating an empty table as a table with
6+
which crashes latex (due to it treating an empty table as a table with
77
external data in the file '' or '.tex')
88
See: https://github.com/nschloe/matplotlib2tikz/pull/134
99
"""
1010
desc = 'BoxPlot'
11-
phash = '6bc6f6b95e0000fe'
11+
phash = '6be3e6b95e8000de'
12+
1213

1314
def plot():
1415
import matplotlib.pyplot as plt

test/testfunctions/dual_axis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
desc = 'Dual axis plot'
44
# phash = '43a35c4b76a94eb8'
5-
phash = '5bab54492628de7c'
5+
phash = '5382544c16bdde75'
66

77

88
def plot():

0 commit comments

Comments
 (0)