Skip to content

Commit 81dd0b1

Browse files
committed
Merge pull request yhat#339 from matthias-k/geom_pointrange
Geom pointrange
2 parents 27d8184 + 3093c4f commit 81dd0b1

File tree

9 files changed

+303
-9
lines changed

9 files changed

+303
-9
lines changed

ggplot/geoms/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
from .geom_hline import geom_hline
1111
from .geom_jitter import geom_jitter
1212
from .geom_line import geom_line
13+
from .geom_linerange import geom_linerange
1314
from .geom_now_its_art import geom_now_its_art
1415
from .geom_path import geom_path
1516
from .geom_point import geom_point
17+
from .geom_pointrange import geom_pointrange
1618
from .geom_rect import geom_rect
1719
from .geom_smooth import geom_smooth
1820
from .geom_step import geom_step
@@ -27,8 +29,8 @@
2729

2830
__facet__ = ['facet_grid', 'facet_wrap']
2931
__geoms__ = ['geom_abline', 'geom_area', 'geom_bar', 'geom_boxplot', 'geom_density',
30-
'geom_histogram', 'geom_hline', 'geom_jitter', 'geom_line',
31-
'geom_now_its_art', 'geom_path', 'geom_point', 'geom_rect',
32+
'geom_histogram', 'geom_hline', 'geom_jitter', 'geom_line', 'geom_linerange',
33+
'geom_now_its_art', 'geom_path', 'geom_point', 'geom_pointrange', 'geom_rect',
3234
'geom_step', 'geom_smooth', 'geom_text', 'geom_tile',
3335
'geom_vline']
3436
__components__ = ['ylab', 'xlab', 'ylim', 'xlim', 'labs', 'ggtitle']

ggplot/geoms/geom_linerange.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import (absolute_import, division, print_function,
2+
unicode_literals)
3+
4+
import sys
5+
6+
from .geom import geom
7+
from ggplot.utils import is_categorical
8+
import numpy as np
9+
10+
11+
class geom_linerange(geom):
12+
"""Plot intervals represented by vertical lines
13+
14+
Parameters
15+
---------
16+
17+
x
18+
x values of data
19+
ymin
20+
lower end of the interval for each x
21+
ymax
22+
upper end of the interval for each x
23+
alpha : float
24+
alpha value, defaults to 1
25+
color : string
26+
line color, defaults to 'black'
27+
linetype : string
28+
line type, defaults to 'solid'
29+
size : string
30+
width of the line, defaults to 2
31+
32+
Examples
33+
--------
34+
35+
.. plot::
36+
:include-source:
37+
38+
import numpy as np
39+
import pandas as pd
40+
from ggplot import *
41+
42+
np.random.seed(42)
43+
x = np.linspace(0.5, 9.5, num=10)
44+
y = np.random.randn(10)
45+
ymin = y - np.random.uniform(0,1, size=10)
46+
ymax = y + np.random.uniform(0,1, size=10)
47+
48+
data = pd.DataFrame({'x': x, 'ymin': ymin, 'ymax': ymax})
49+
50+
ggplot(aes(x='x', ymin='ymin', ymax='ymax'), data) \
51+
+ geom_linerange()
52+
53+
"""
54+
DEFAULT_AES = {'alpha': 1, 'color': 'black',
55+
'linetype': 'solid',
56+
'size': 2}
57+
REQUIRED_AES = {'x', 'ymin', 'ymax'}
58+
DEFAULT_PARAMS = {'stat': 'identity', 'position': 'identity', 'cmap': None}
59+
60+
_aes_renames = {'size': 'linewidth', 'linetype': 'linestyle'}
61+
_units = {'alpha', 'color', 'linestyle'}
62+
63+
def __init__(self, *args, **kwargs):
64+
super(geom_linerange, self).__init__(*args, **kwargs)
65+
self._warning_printed = False
66+
67+
def _plot_unit(self, pinfo, ax):
68+
# If x is categorical, calculate positions to plot
69+
categorical = is_categorical(pinfo['x'])
70+
if categorical:
71+
x = pinfo.pop('x')
72+
new_x = np.arange(len(x))
73+
ax.set_xticks(new_x)
74+
ax.set_xticklabels(x)
75+
pinfo['x'] = new_x
76+
77+
if 'linewidth' in pinfo and isinstance(pinfo['linewidth'], list):
78+
# ggplot also supports aes(size=...) but the current mathplotlib
79+
# is not. See https://github.com/matplotlib/matplotlib/issues/2658
80+
pinfo['linewidth'] = 4
81+
if not self._warning_printed:
82+
msg = "'geom_line()' currenty does not support the mapping of " +\
83+
"size ('aes(size=<var>'), using size=4 as a replacement.\n" +\
84+
"Use 'geom_line(size=x)' to set the size for the whole line.\n"
85+
sys.stderr.write(msg)
86+
self._warning_printed = True
87+
88+
x = pinfo.pop('x')
89+
x = np.vstack([x, x])
90+
91+
ymin = pinfo.pop('ymin')
92+
ymax = pinfo.pop('ymax')
93+
y = np.vstack([ymin, ymax])
94+
95+
ax.plot(x, y, **pinfo)

ggplot/geoms/geom_point.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ class geom_point(geom):
1515

1616
def _plot_unit(self, pinfo, ax):
1717

18-
_abscent = {None: pinfo['color'], False: ''}
19-
try:
20-
if pinfo['facecolor'] in _abscent:
21-
pinfo['facecolor'] = _abscent[pinfo['facecolor']]
22-
except TypeError:
23-
pass
18+
fc = pinfo['facecolor']
19+
if fc is None:
20+
# default to color
21+
pinfo['facecolor'] = pinfo['color']
22+
elif fc is False:
23+
# Matlab expects empty string instead of False
24+
pinfo['facecolor'] = ''
2425

2526
# for some reason, scatter doesn't default to the same color styles
2627
# as the axes.color_cycle

ggplot/geoms/geom_pointrange.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from __future__ import (absolute_import, division, print_function,
2+
unicode_literals)
3+
4+
import sys
5+
6+
import matplotlib as mpl
7+
from .geom import geom
8+
from ggplot.utils import is_categorical
9+
import numpy as np
10+
11+
12+
class geom_pointrange(geom):
13+
"""Plot intervals represented by vertical lines with a point in each interval
14+
15+
Parameters
16+
---------
17+
18+
x
19+
x values of data
20+
y
21+
y value of the point for each x
22+
ymin
23+
lower end of the interval for each x
24+
ymax
25+
upper end of the interval for each x
26+
alpha : float
27+
alpha value, defaults to 1
28+
color : string
29+
line color, defaults to 'black'
30+
fill : string
31+
Fill type for the points, defaults to 'None'
32+
linetype : string
33+
line type, defaults to 'solid'
34+
shape : string
35+
shape of the points, defaults to 'o' (i.e. circles)
36+
size : string
37+
width of the line and size of the point, defaults to 2
38+
39+
Examples
40+
--------
41+
42+
.. plot::
43+
:include-source:
44+
45+
import numpy as np
46+
import pandas as pd
47+
from ggplot import *
48+
49+
np.random.seed(42)
50+
x = np.linspace(0.5, 9.5, num=10)
51+
y = np.random.randn(10)
52+
ymin = y - np.random.uniform(0,1, size=10)
53+
ymax = y + np.random.uniform(0,1, size=10)
54+
55+
data = pd.DataFrame({'x': x, 'y': y, 'ymin': ymin, 'ymax': ymax})
56+
57+
ggplot(aes(x='x', y='y', ymin='ymin', ymax='ymax'), data) \
58+
+ geom_pointrange()
59+
60+
"""
61+
62+
DEFAULT_AES = {'alpha': 1, 'color': 'black', 'fill': None,
63+
'linetype': 'solid',
64+
'shape': 'o',
65+
'size': 2}
66+
REQUIRED_AES = {'x', 'y', 'ymin', 'ymax'}
67+
DEFAULT_PARAMS = {'stat': 'identity', 'position': 'identity', 'cmap': None}
68+
69+
_aes_renames = {'size': 'linewidth', 'linetype': 'linestyle', 'shape': 'marker', 'fill': 'facecolor'}
70+
_units = {'alpha', 'color', 'linestyle', 'marker'}
71+
72+
def __init__(self, *args, **kwargs):
73+
super(geom_pointrange, self).__init__(*args, **kwargs)
74+
self._warning_printed = False
75+
76+
def _plot_unit(self, pinfo, ax):
77+
# If x is categorical, calculate positions to plot
78+
categorical = is_categorical(pinfo['x'])
79+
if categorical:
80+
x = pinfo.pop('x')
81+
new_x = np.arange(len(x))
82+
ax.set_xticks(new_x)
83+
ax.set_xticklabels(x)
84+
pinfo['x'] = new_x
85+
86+
if 'linewidth' in pinfo and isinstance(pinfo['linewidth'], list):
87+
# ggplot also supports aes(size=...) but the current mathplotlib
88+
# is not. See https://github.com/matplotlib/matplotlib/issues/2658
89+
pinfo['linewidth'] = 4
90+
if not self._warning_printed:
91+
msg = "'geom_line()' currenty does not support the mapping of " +\
92+
"size ('aes(size=<var>'), using size=4 as a replacement.\n" +\
93+
"Use 'geom_line(size=x)' to set the size for the whole line.\n"
94+
sys.stderr.write(msg)
95+
self._warning_printed = True
96+
97+
# Plotting the line
98+
pinfoline = dict(pinfo)
99+
del pinfoline['marker']
100+
del pinfoline['facecolor']
101+
del pinfoline['y']
102+
103+
x = pinfoline.pop('x')
104+
x = np.vstack([x, x])
105+
106+
ymin = pinfoline.pop('ymin')
107+
ymax = pinfoline.pop('ymax')
108+
y = np.vstack([ymin, ymax])
109+
110+
ax.plot(x, y, **pinfoline)
111+
112+
# Plotting the points
113+
pinfopoint = dict(pinfo)
114+
del pinfopoint['ymin']
115+
del pinfopoint['ymax']
116+
del pinfopoint['linestyle']
117+
118+
fc = pinfopoint['facecolor']
119+
if fc is None:
120+
# default to color
121+
pinfopoint['facecolor'] = pinfopoint['color']
122+
elif fc is False:
123+
# Matlab expects empty string instead of False
124+
pinfopoint['facecolor'] = ''
125+
126+
# for some reason, scatter doesn't default to the same color styles
127+
# as the axes.color_cycle
128+
if "color" not in pinfopoint and self.params['cmap'] is None:
129+
pinfopoint["color"] = mpl.rcParams.get("axes.color_cycle", ["#333333"])[0]
130+
131+
pinfopoint['s'] = pinfopoint.pop('linewidth')**2*4
132+
133+
ax.scatter(**pinfopoint)

ggplot/tests/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def teardown_package():
3434
'ggplot.tests.test_geom_bar',
3535
'ggplot.tests.test_qplot',
3636
'ggplot.tests.test_geom_lines',
37+
'ggplot.tests.test_geom_linerange',
38+
'ggplot.tests.test_geom_pointrange',
3739
'ggplot.tests.test_faceting',
3840
'ggplot.tests.test_stat_function',
3941
'ggplot.tests.test_scale_facet_wrap',
@@ -101,11 +103,14 @@ def make_test_fn(fname, purpose):
101103
base, ext = os.path.splitext(fname)
102104
return '%s-%s%s' % (base, purpose, ext)
103105
expected_fname = make_test_fn(actual_fname, 'expected')
106+
# Save the figure before testing whether the original image
107+
# actually exists. This make creating new tests much easier,
108+
# as the result image can afterwards just be copied.
109+
fig.savefig(actual_fname)
104110
if os.path.exists(orig_expected_fname):
105111
shutil.copyfile(orig_expected_fname, expected_fname)
106112
else:
107113
raise Exception("Baseline image %s is missing" % orig_expected_fname)
108-
fig.savefig(actual_fname)
109114
err = compare_images(expected_fname, actual_fname,
110115
tol, in_decorator=True)
111116
if err:
Loading
Loading

ggplot/tests/test_geom_linerange.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import (absolute_import, division, print_function,
2+
unicode_literals)
3+
4+
from . import get_assert_same_ggplot, cleanup
5+
assert_same_ggplot = get_assert_same_ggplot(__file__)
6+
7+
from ggplot import *
8+
9+
import numpy as np
10+
import pandas as pd
11+
12+
13+
def _build_testing_df():
14+
rst = np.random.RandomState(42)
15+
x = np.linspace(0.5, 9.5, num=10)
16+
y = rst.randn(10)
17+
ymin = y - rst.uniform(0, 1, size=10)
18+
ymax = y + rst.uniform(0, 1, size=10)
19+
20+
df = pd.DataFrame({'x': x, 'y': y, 'ymin': ymin, 'ymax': ymax})
21+
return df
22+
23+
24+
@cleanup
25+
def test_geom_linerange():
26+
df = _build_testing_df()
27+
gg = ggplot(aes(x="x", y="y", ymin="ymin", ymax="ymax"), data=df)
28+
gg = gg + geom_linerange()
29+
assert_same_ggplot(gg, "geom_linerange")

ggplot/tests/test_geom_pointrange.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import (absolute_import, division, print_function,
2+
unicode_literals)
3+
4+
from . import get_assert_same_ggplot, cleanup
5+
assert_same_ggplot = get_assert_same_ggplot(__file__)
6+
7+
from ggplot import *
8+
9+
import numpy as np
10+
import pandas as pd
11+
12+
13+
def _build_testing_df():
14+
rst = np.random.RandomState(42)
15+
x = np.linspace(0.5, 9.5, num=10)
16+
y = rst.randn(10)
17+
ymin = y - rst.uniform(0, 1, size=10)
18+
ymax = y + rst.uniform(0, 1, size=10)
19+
20+
df = pd.DataFrame({'x': x, 'y': y, 'ymin': ymin, 'ymax': ymax})
21+
return df
22+
23+
24+
@cleanup
25+
def test_geom_pointrange():
26+
df = _build_testing_df()
27+
gg = ggplot(aes(x="x", y="y", ymin="ymin", ymax="ymax"), data=df)
28+
gg = gg + geom_pointrange()
29+
assert_same_ggplot(gg, "geom_pointrange")

0 commit comments

Comments
 (0)