├── .gitignore ├── .hgignore ├── .hgtags ├── LICENSE.txt ├── README.md ├── build └── lib │ └── pyggplot │ ├── __init__.py │ ├── base.py │ ├── plot_nine.py │ ├── plot_r.py │ └── svg_stack.py ├── pyggplot ├── __init__.py ├── base.py ├── plot_nine.py ├── plot_r.py └── svg_stack.py ├── register.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | 4 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax:glob 2 | build/* 3 | *.so 4 | *.pyc 5 | dist/* 6 | pyggplot.egg-info/* 7 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | bafecf127fc7d86ee5579ea2ef66e56de0f88ea0 master 2 | bafecf127fc7d86ee5579ea2ef66e56de0f88ea0 master 3 | fb61b2a95cf9f931f7b343e55021a73b93be15c2 master 4 | fb61b2a95cf9f931f7b343e55021a73b93be15c2 master 5 | 0fbf60496c4068552041f06ca061dfd490b1bdc6 master 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ## Copyright (c) 2009-2015, Florian Finkernagel. All rights reserved. 2 | 3 | ## Redistribution and use in source and binary forms, with or without 4 | ## modification, are permitted provided that the following conditions are 5 | ## met: 6 | 7 | ## * Redistributions of source code must retain the above copyright 8 | ## notice, this list of conditions and the following disclaimer. 9 | 10 | ## * Redistributions in binary form must reproduce the above 11 | ## copyright notice, this list of conditions and the following 12 | ## disclaimer in the documentation and/or other materials provided 13 | ## with the distribution. 14 | 15 | ## * Neither the name of the Andrew Straw nor the names of its 16 | ## contributors may be used to endorse or promote products derived 17 | ## from this software without specific prior written permission. 18 | 19 | ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | ## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | ## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyggplot 2 | ======== 3 | 4 | pyggplot is a Pythonic wrapper around the [R ggplot2 library](http://had.co.nz/ggplot2/). 5 | 6 | It is based on a a straightforward *take [Pandas](http://pandas.pydata.org/) data frames and shove them into [R](http://www.r-project.org/) via [rpy2](https://pypi.python.org/pypi/rpy2)* approach. 7 | 8 | ## Examples 9 | Please visit http://nbviewer.ipython.org/url/tyberiusprime.github.io/pyggplot/pyggplot%20samples.ipynb 10 | 11 | ## Installation 12 | 13 | The easiest installation is via [PyPI](https://pypi.python.org/pypi). 14 | 15 | $ pip install pyggplot 16 | 17 | You may be required to update `pandas`, `rpy2`, so you may be required to run 18 | 19 | $ pip install --upgrade pyggplot 20 | 21 | ## Usage 22 | 23 | import pandas as pd 24 | import numpy as np 25 | import ggplot 26 | 27 | df = pd.DataFrame({'x': np.random.rand(100), 28 | 'y': np.random.randn(100), 29 | 'group': ['A','B'] * 50}) 30 | 31 | p = pyggplot.Plot(df) 32 | p.add_scatter('x','y', color='group') 33 | p.render('output.png') 34 | ## or if you want to use it in IPython Notebook 35 | # p.render_notebook() 36 | 37 | 38 | 39 | ## Further usage 40 | 41 | Takes a `pandas.DataFrame` object, then add layers with the various `add_xyz` 42 | functions (e.g. `add_scatter`). 43 | 44 | Refer to the ggplot documentation about the layers (geoms), and simply 45 | replace `geom_*` with `add_*`. 46 | See: http://docs.ggplot2.org/0.9.3.1/index.html 47 | 48 | You do not need to separate aesthetics from values - the wrapper 49 | will treat a parameter as value if and only if it is not a column name. 50 | (so `y = 0` is a value, `color = 'blue'` is a value - except if you have a column `'blue'`, then it is a column!. 51 | And `y = 'value'` does not work, but that seems to be a ggplot issue). 52 | 53 | When the DataFrame is passed to R: 54 | 55 | * row indices are turned into columns with 'reset_index', 56 | * multi level column indices are flattened by concatenating them with `' '`, that is `(X, 'mean')` becomes `'x mean'`. 57 | 58 | Error messages are not great - most of them translate to 'one or more columns were not found', 59 | but they can appear as a lot of different actual messages such as 60 | 61 | * argument "env" is missing, with no default 62 | * object 'y' not found 63 | * object 'dat_0' not found 64 | * requires the following missing aesthetics: x 65 | * non numeric argument to binary operator 66 | 67 | without actually quite pointing at what is strictly the offending value. 68 | Also, the error appears when rendering (or printing in the [IPython Notebook](http://ipython.org/notebook.html)), 69 | not when adding the layer. 70 | 71 | ## Open questions 72 | 73 | * the stat support is not great - it doesn't easily map into pythonic objects. For now, do your stats in pandas - more powerful anyhow! 74 | * how could error messages be improved? 75 | 76 | 77 | 78 | ## Other ggplots' for python 79 | 80 | * http://ggplot.yhathq.com/ is a port of ggplot2 for python based on matplotlib - unfortunatly not yet feature complete as of early 2015. 81 | * https://github.com/sirrice/pyplot is another wrapper for ggplot closer to R's syntax, and does not rely on rpy2 - calls command line R. 82 | 83 | 84 | -------------------------------------------------------------------------------- /build/lib/pyggplot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2015, Florian Finkernagel. All rights reserved. 2 | 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are 5 | # met: 6 | 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | 15 | # * Neither the name of the Andrew Straw nor the names of its 16 | # contributors may be used to endorse or promote products derived 17 | # from this software without specific prior written permission. 18 | 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | """A wrapper around ggplot2 ( http://had.co.nz/ggplot2/ ) and 32 | plotnine (https://plotnine.readthedocs.io/en/stable/index.html ) 33 | 34 | Takes a pandas.DataFrame object, then add layers with the various add_xyz 35 | functions (e.g. add_scatter). 36 | 37 | Referr to the ggplot/plotnine documentation about the layers (geoms), and simply 38 | replace geom_* with add_*. 39 | See http://docs.ggplot2.org/0.9.3.1/index.html or 40 | https://plotnine.readthedocs.io/en/stable/index.html 41 | 42 | You do not need to seperate aesthetics from values - the wrapper 43 | will treat a parameter as value if and only if it is not a column name. 44 | (so y = 0 is a value, color = 'blue 'is a value - except if you have a column 45 | 'blue', then it's a column!. And y = 'value' doesn't work, but that seems to be a ggplot issue). 46 | 47 | When the DataFrame is passed to the plotting library: 48 | - row indices are truned into columns with 'reset_index' 49 | - multi level column indices are flattend by concatenating them with ' ' 50 | -> (X, 'mean) becomes 'x mean' 51 | 52 | R Error messages are not great - most of them translate to 'one or more columns were not found', 53 | but they can appear as a lot of different actual messages such as 54 | - argument "env" is missing, with no defalut 55 | - object 'y' not found 56 | - object 'dat_0' not found 57 | - requires the follewing missing aesthetics: x 58 | - non numeric argument to binary operator 59 | without actually quite pointing at what is strictly the offending value. 60 | Also, the error appears when rendering (or printing in ipython notebook), 61 | not when adding the layer. 62 | """ 63 | #from .plot_r import Plot, plot_heatmap, multiplot, MultiPagePlot, convert_dataframe_to_r 64 | from .base import _PlotBase 65 | #from . import plot_nine 66 | from .plot_nine import Plot, Expression, Scalar 67 | 68 | all = [Plot, Expression, Scalar] 69 | -------------------------------------------------------------------------------- /build/lib/pyggplot/base.py: -------------------------------------------------------------------------------- 1 | class _PlotBase(object): 2 | pass 3 | -------------------------------------------------------------------------------- /build/lib/pyggplot/plot_nine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import pandas as pd 4 | import numpy as np 5 | import itertools 6 | from .base import _PlotBase 7 | #try: 8 | #import exptools 9 | #exptools.load_software('palettable') 10 | #exptools.load_software('descartes') 11 | #exptools.load_software('mizani') 12 | #exptools.load_software('patsy') 13 | #exptools.load_software('plotnine') 14 | 15 | #except ImportError: 16 | #pass 17 | import matplotlib 18 | matplotlib.use('agg') 19 | import plotnine as p9 20 | from plotnine import stats 21 | 22 | 23 | class Expression: 24 | def __init__(self, expr_str): 25 | self.expr_str = expr_str 26 | 27 | 28 | class Scalar: 29 | def __init__(self, scalar_str): 30 | self.scalar_str = scalar_str 31 | 32 | 33 | class Plot(_PlotBase): 34 | def __init__(self, dataframe): 35 | self.dataframe = self._prep_dataframe(dataframe) 36 | self.ipython_plot_width = 600 37 | self.ipython_plot_height = 600 38 | self.plot = p9.ggplot(self.dataframe) 39 | self._add_geoms() 40 | self._add_scales_and_cords_and_facets_and_themes() 41 | self._add_positions_and_stats() 42 | 43 | def __add__(self, other): 44 | if hasattr(other, '__radd__'): 45 | # let's assume that it's a plotnine object that knows how to radd it 46 | # self to our plot 47 | self.plot = self.plot + other 48 | return self 49 | 50 | 51 | def _prep_dataframe(self, df): 52 | """prepare the dataframe by making sure it's a pandas dataframe, 53 | has no multi index columns, has no index etc 54 | """ 55 | if 'pydataframe.dataframe.DataFrame' in str(type(df)): 56 | df = self._convert_pydataframe(df) 57 | elif isinstance(df, dict): 58 | df = pd.DataFrame(df) 59 | elif isinstance(df, pd.Series): 60 | df = pd.DataFrame(df) 61 | if isinstance(df.columns, pd.MultiIndex): 62 | df.columns = [' '.join(col).strip() for col in df.columns.values] 63 | df = df.reset_index() 64 | return df 65 | 66 | def _convert_pydataframe(self, pdf): 67 | """Compability shim for still being able to use old pydataframes with the new pandas interface""" 68 | d = {} 69 | for column in pdf.columns_ordered: 70 | o = pdf.gcv(column) 71 | if 'pydataframe.factors.Factor' in str(type(o)): 72 | d[column] = pd.Series( 73 | pd.Categorical(o.as_levels(), categories=o.levels)) 74 | else: 75 | d[column] = o 76 | return pd.DataFrame(d) 77 | 78 | def _repr_png_(self, width=None, height=None): 79 | """Show the plot in the ipython notebook (ie. return png formated image data)""" 80 | if width is None: 81 | width = self.ipython_plot_width 82 | height = self.ipython_plot_height 83 | try: 84 | handle, name = tempfile.mkstemp( 85 | suffix=".png" 86 | ) # mac os for some reason would not read back again from a named tempfile. 87 | os.close(handle) 88 | self.plot.save( 89 | name, 90 | width=width / 72., 91 | height=height / 72., 92 | dpi=72, 93 | verbose=False) 94 | tf = open(name, "rb") 95 | result = tf.read() 96 | tf.close() 97 | return result 98 | finally: 99 | os.unlink(name) 100 | 101 | def _repr_svg_(self, width=None, height=None): 102 | """Show the plot in the ipython notebook (ie. return svg formated image data)""" 103 | if width is None: 104 | width = self.ipython_plot_width / 150. * 72 105 | height = self.ipython_plot_height / 150. * 72 106 | try: 107 | handle, name = tempfile.mkstemp( 108 | suffix=".svg" 109 | ) # mac os for some reason would not read back again from a named tempfile. 110 | os.close(handle) 111 | self.plot.save( 112 | name, 113 | width=width / 72., 114 | height=height / 72., 115 | dpi=72, 116 | verbose=False) 117 | tf = open(name, "rb") 118 | result = tf.read().decode('utf-8') 119 | tf.close() 120 | # newer jupyters need the height attribute 121 | # otherwise the iframe is only one line high and 122 | # the figure tiny 123 | result = result.replace("viewBox=", 124 | 'height=\'%i\' viewBox=' % (height)) 125 | return result, {"isolated": True} 126 | finally: 127 | os.unlink(name) 128 | 129 | #def geom_scatter(self, x, y): 130 | #self.plot += p9.geom_point(p9.aes(x, y)) 131 | 132 | def _add(self, geom_class, args, kwargs): 133 | """The generic method to add a geom to the ggplot. 134 | You need to call add_xyz (see _add_geom_methods for a list, with each variable mapping 135 | being one argument) with the respectivly required parameters (see ggplot documentation). 136 | You may optionally pass in an argument called data, which will replace the plot-global dataframe 137 | for this particular geom 138 | """ 139 | 140 | if 'data' in kwargs: 141 | data = self._prep_dataframe(kwargs['data']) 142 | else: 143 | data = None 144 | if 'stat' in kwargs: 145 | stat = kwargs['stat'] 146 | else: 147 | stat = geom_class.DEFAULT_PARAMS['stat'] 148 | if isinstance(stat, str): 149 | stat = getattr( 150 | p9.stats, 'stat_' + stat 151 | if not stat.startswith('stat_') else stat) 152 | mapping = {} 153 | out_kwargs = {} 154 | all_defined_mappings = list( 155 | stat.REQUIRED_AES) + list(geom_class.REQUIRED_AES) + list( 156 | geom_class.DEFAULT_AES) + ['group'] # + list(geom_class.DEFAULT_PARAMS) 157 | if 'x' in geom_class.REQUIRED_AES: 158 | if len(args) > 0: 159 | kwargs['x'] = args[0] 160 | if 'y' in geom_class.REQUIRED_AES or 'y' in stat.REQUIRED_AES: 161 | if len(args) > 1: 162 | kwargs['y'] = args[1] 163 | if len(args) > 2: 164 | raise ValueError( 165 | "We only accept x&y by position, all other args need to be named" 166 | ) 167 | else: 168 | if len(args) > 1: 169 | raise ValueError( 170 | "We only accept x by position, all other args need to be named" 171 | ) 172 | elif 'xmin' and 'ymin' in geom_class.REQUIRED_AES: 173 | if len(args) > 0: 174 | kwargs['xmin'] = args[0] 175 | if len(args) > 1: 176 | kwargs['xmax'] = args[1] 177 | if len(args) > 2: 178 | kwargs['ymin'] = args[2] 179 | if len(args) > 3: 180 | kwargs['ymax'] = args[3] 181 | elif len(args) > 4: 182 | raise ValueError( 183 | "We only accept xmin,xmax,ymin,ymax by position, all other args need to be named" 184 | ) 185 | 186 | for a, b in kwargs.items(): 187 | if a in all_defined_mappings: 188 | # if it is an expression, keep it that way 189 | # if it's a single value, treat it as a scalar 190 | # except if it looks like an expression (ie. has () 191 | is_kwarg = False 192 | if isinstance(b, Expression): 193 | b = b.expr_str 194 | is_kwarg = True 195 | elif isinstance(b, Scalar): 196 | b = b.scalar_str 197 | is_kwarg = True 198 | elif (((data is not None and b not in data.columns) or 199 | (data is None and b not in self.dataframe.columns)) 200 | and not '(' in str( 201 | b) # so a true scalar, not a calculated expression 202 | ): 203 | b = b # which will tell it to treat it as a scalar! 204 | is_kwarg = True 205 | if not is_kwarg: 206 | mapping[a] = b 207 | else: 208 | out_kwargs[a] = b 209 | 210 | #mapping.update({x: kwargs[x] for x in kwargs if x in all_defined_mappings}) 211 | 212 | out_kwargs['data'] = data 213 | for a in geom_class.DEFAULT_PARAMS: 214 | if a in kwargs: 215 | out_kwargs[a] = kwargs[a] 216 | 217 | self.plot += geom_class(mapping=p9.aes(**mapping), **out_kwargs) 218 | return self 219 | 220 | def _add_geoms(self): 221 | # allow aliases 222 | name_to_geom = {} 223 | for name in dir(p9): 224 | if name.startswith('geom_'): 225 | geom = getattr(p9, name) 226 | name_to_geom[name] = geom 227 | name_to_geom['geom_scatter'] = name_to_geom['geom_point'] 228 | for name, geom in name_to_geom.items(): 229 | 230 | def define(geom): 231 | def do_add(*args, **kwargs): 232 | return self._add(geom_class=geom, args=args, kwargs=kwargs) 233 | 234 | do_add.__doc__ = geom.__doc__ 235 | return do_add 236 | 237 | method_name = 'add_' + name[5:] 238 | if not hasattr(self, method_name): 239 | setattr(self, method_name, define(geom)) 240 | 241 | return self 242 | 243 | def _add_scales_and_cords_and_facets_and_themes(self): 244 | for name in dir(p9): 245 | if name.startswith('scale_') or name.startswith( 246 | 'coord_') or name.startswith('facet') or name.startswith( 247 | 'theme'): 248 | method_name = name 249 | if not hasattr(self, method_name): 250 | scale = getattr(p9, name) 251 | 252 | def define(scale): 253 | def add_(*args, **kwargs): 254 | self.plot += scale(*args, **kwargs) 255 | return self 256 | 257 | add_.__doc__ = scale.__doc__ 258 | return add_ 259 | 260 | setattr(self, method_name, define(scale)) 261 | 262 | def _add_positions_and_stats(self): 263 | for name in dir(p9): 264 | if name.startswith('stat_') or name.startswith('position'): 265 | setattr(self, name, getattr(p9, name)) 266 | 267 | def title(self, text): 268 | """Set plot title""" 269 | self.plot += p9.ggtitle(text) 270 | 271 | def set_title(self, text): 272 | """Set plot title""" 273 | self.title(text) 274 | 275 | def facet(self, *args, **kwargs): 276 | """Compability to old calling style""" 277 | if 'free_y' in kwargs['scales']: 278 | self.plot += p9.theme(subplots_adjust={'wspace':0.2}) 279 | return self.facet_wrap(*args, **kwargs) 280 | 281 | def add_jitter(self, x, y, jitter_x=True, jitter_y=True, **kwargs): 282 | # an api changed in ggplot necessitates this - jitter_x and jitter_y have been replaced with position_jitter(width, height)... 283 | kwargs['position'] = self.position_jitter( 284 | width=0.4 if jitter_x is True else float(jitter_x), 285 | height=0.4 if jitter_y is True else float(jitter_y), 286 | ) 287 | self.add_point(x, y, **kwargs) 288 | 289 | def add_cummulative(self, 290 | x_column, 291 | ascending=True, 292 | percent=False, 293 | percentile=1.0): 294 | """Add a line showing cumulative % of data <= x. 295 | if you specify a percentile, all data at the extreme range is dropped 296 | 297 | 298 | """ 299 | total = 0 300 | current = 0 301 | column_data = self.dataframe[x_column].copy() # explicit copy! 302 | column_data = column_data[~np.isnan(column_data)] 303 | column_data = np.sort(column_data) 304 | total = float(len(column_data)) 305 | real_total = total 306 | if not ascending: 307 | column_data = column_data[::-1] # numpy.reverse(column_data) 308 | if percentile != 1.0: 309 | if ascending: 310 | maximum = np.max(column_data) 311 | else: 312 | maximum = np.min(column_data) 313 | total = float(total * percentile) 314 | if total > 0: 315 | column_data = column_data[:total] 316 | offset = real_total - total 317 | else: 318 | column_data = column_data[total:] 319 | offset = 2 * abs(total) 320 | else: 321 | offset = 0 322 | x_values = [] 323 | y_values = [] 324 | if percent: 325 | current = 100.0 326 | else: 327 | current = total 328 | for value, group in itertools.groupby(column_data): 329 | x_values.append(value) 330 | y_values.append(current + offset) 331 | if percent: 332 | current -= (len(list(group)) / total) 333 | else: 334 | current -= (len(list(group))) 335 | # y_values.append(current) 336 | data = pd.DataFrame({ 337 | x_column: x_values, 338 | ("%" if percent else '#') + ' <=': y_values 339 | }) 340 | if percentile > 0: 341 | self.scale_x_continuous( 342 | limits=[0, real_total if not percent else 100]) 343 | self.add_line(x_column, ("%" if percent else '#') + ' <=', data=data) 344 | if percentile != 1.0: 345 | self.set_title('showing only %.2f percentile, extreme was %.2f' % 346 | (percentile, maximum)) 347 | return self 348 | 349 | def add_heatmap(self, 350 | x_column, 351 | y_column, 352 | fill, 353 | low="red", 354 | mid=None, 355 | high="blue", 356 | midpoint=0, 357 | guide_legend=None, 358 | scale_args=None): 359 | self.add_tile(x_column, y_column, fill=fill) 360 | if mid is None: 361 | self.scale_fill_gradient(low=low, high=high, **scale_args) 362 | else: 363 | self.scale_fill_gradient2( 364 | low=low, mid=mid, high=high, midpoint=midpoint, **scale_args) 365 | 366 | def add_alternating_background(self, 367 | x_column, 368 | fill_1="#EEEEEE", 369 | fill_2="#FFFFFF", 370 | vertical=False, 371 | alpha=0.5, 372 | log_y_scale=False, 373 | facet_column=None): 374 | """Add an alternating background to a categorial (x-axis) plot. 375 | """ 376 | self.scale_x_discrete() 377 | if log_y_scale: 378 | self._expected_y_scale = 'log' 379 | else: 380 | self._expected_y_scale = 'normal' 381 | if facet_column is None: 382 | sub_frames = [(False, self.dataframe)] 383 | else: 384 | sub_frames = self.dataframe.groupby(facet_column) 385 | 386 | for facet_value, facet_df in sub_frames: 387 | no_of_x_values = len(facet_df[x_column].unique()) 388 | df_rect = pd.DataFrame({ 389 | 'xmin': 390 | np.array(range(no_of_x_values)) - .5 + 1, 391 | 'xmax': 392 | np.array(range(no_of_x_values)) + .5 + 1, 393 | 'ymin': 394 | -np.inf if not log_y_scale else 0, 395 | 'ymax': 396 | np.inf, 397 | 'fill': 398 | ([fill_1, fill_2] * (no_of_x_values // 2 + 1))[:no_of_x_values] 399 | }) 400 | if facet_value is not False: 401 | df_rect.insert(0, facet_column, facet_value) 402 | #df_rect.insert(0, 'alpha', alpha) 403 | left = df_rect[df_rect.fill == fill_1] 404 | right = df_rect[df_rect.fill == fill_2] 405 | if not vertical: 406 | if len(left): 407 | self.add_rect( 408 | 'xmin', 409 | 'xmax', 410 | 'ymin', 411 | 'ymax', 412 | fill='fill', 413 | data=left, 414 | alpha=alpha) 415 | if len(right): 416 | self.add_rect( 417 | 'xmin', 418 | 'xmax', 419 | 'ymin', 420 | 'ymax', 421 | fill='fill', 422 | data=right, 423 | alpha=alpha) 424 | else: 425 | if len(left): 426 | self.add_rect( 427 | 'ymin', 428 | 'ymax', 429 | 'xmin', 430 | 'xmax', 431 | fill='fill', 432 | data=left, 433 | alpha=alpha) 434 | if len(right): 435 | self.add_rect( 436 | 'ymin', 437 | 'ymax', 438 | 'xmin', 439 | 'xmax', 440 | fill='fill', 441 | data=right, 442 | alpha=alpha) 443 | self.scale_fill_identity() 444 | self.scale_alpha_identity() 445 | return self 446 | 447 | def turn_x_axis_labels(self, 448 | angle=90, 449 | hjust='center', 450 | vjust='top', 451 | size=None, 452 | color=None): 453 | return self.turn_axis_labels('axis_text_x', angle, hjust, vjust, size, 454 | color) 455 | 456 | def turn_y_axis_labels(self, 457 | angle=90, 458 | hjust='hjust', 459 | vjust='center', 460 | size=None, 461 | color=None): 462 | return self.turn_axis_labels('axis_text_y', angle, hjust, vjust, size, 463 | color) 464 | 465 | def _change_theme(self, what, t): 466 | if self.plot.theme is None: 467 | self.theme_grey() 468 | self.plot.theme += p9.theme(**{what: t}) 469 | return self 470 | 471 | def turn_axis_labels(self, ax, angle, hjust, vjust, size, color): 472 | t = p9.themes.element_text( 473 | rotation=angle, ha=hjust, va=vjust, size=size, color=color) 474 | return self._change_theme(ax, t) 475 | 476 | def hide_background(self): 477 | return self._change_theme('panel_background', p9.element_blank()) 478 | 479 | def hide_y_axis_labels(self): 480 | return self._change_theme('axis_text_y', p9.element_blank()) 481 | 482 | def hide_x_axis_labels(self): 483 | return self._change_theme('axis_text_x', p9.element_blank()) 484 | 485 | def hide_axis_ticks(self): 486 | return self._change_theme('axis_ticks', p9.element_blank()) 487 | 488 | def hide_axis_ticks_x(self): 489 | return self._change_theme('axis_ticks_major_x', p9.element_blank()) 490 | 491 | def hide_axis_ticks_y(self): 492 | return self._change_theme('axis_ticks_major_y', p9.element_blank()) 493 | 494 | def hide_y_axis_title(self): 495 | return self._change_theme('axis_title_y', p9.element_blank()) 496 | 497 | def hide_x_axis_title(self): 498 | return self._change_theme('axis_title_x', p9.element_blank()) 499 | 500 | def hide_facet_labels(self): 501 | self._change_theme('strip_background', p9.element_blank()) 502 | return self._change_theme('strip_text_x', p9.element_blank()) 503 | 504 | def hide_legend_key(self): 505 | raise ValueError("plotnine doesn't do 'hide_legend' - pass show_legend=False to the geoms instead") 506 | 507 | _many_cat_colors = [ 508 | "#1C86EE", 509 | "#E31A1C", # red 510 | "#008B00", 511 | "#6A3D9A", # purple 512 | "#FF7F00", # orange 513 | "#4D4D4D", 514 | "#FFD700", 515 | "#7EC0EE", 516 | "#FB9A99", # lt pink 517 | "#90EE90", 518 | # "#CAB2D6", # lt purple 519 | "#0000FF", 520 | "#FDBF6F", # lt orange 521 | "#B3B3B3", 522 | "EEE685", 523 | "#B03060", 524 | "#FF83FA", 525 | "#FF1493", 526 | "#0000FF", 527 | "#36648B", 528 | "#00CED1", 529 | "#00FF00", 530 | "#8B8B00", 531 | "#CDCD00", 532 | "#8B4500", 533 | "#A52A2A" 534 | ] 535 | 536 | def scale_fill_many_categories(self, offset=0, **kwargs): 537 | self.scale_fill_manual((self._many_cat_colors + self._many_cat_colors 538 | )[offset:offset + len(self._many_cat_colors)], **kwargs) 539 | return self 540 | 541 | def scale_color_many_categories(self, offset=0, **kwargs): 542 | self.scale_color_manual((self._many_cat_colors + self._many_cat_colors 543 | )[offset:offset + len(self._many_cat_colors)], **kwargs) 544 | return self 545 | 546 | def render(self, 547 | output_filename, 548 | width=8, 549 | height=6, 550 | dpi=300, 551 | din_size=None): 552 | if din_size == 'A4': 553 | width = 8.267 554 | height = 11.692 555 | self.plot += p9.theme(dpi=dpi) 556 | self.plot.save(filename=output_filename, width=width, height=height, verbose=False) 557 | 558 | 559 | save_as_pdf_pages = p9.save_as_pdf_pages 560 | -------------------------------------------------------------------------------- /build/lib/pyggplot/svg_stack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## Copyright (c) 2009 Andrew D. Straw 4 | 5 | ## Permission is hereby granted, free of charge, to any person obtaining a copy 6 | ## of this software and associated documentation files (the "Software"), to deal 7 | ## in the Software without restriction, including without limitation the rights 8 | ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | ## copies of the Software, and to permit persons to whom the Software is 10 | ## furnished to do so, subject to the following conditions: 11 | 12 | ## The above copyright notice and this permission notice shall be included in 13 | ## all copies or substantial portions of the Software. 14 | 15 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | ## THE SOFTWARE. 22 | 23 | from lxml import etree # Ubuntu Karmic package: python-lxml 24 | import sys, re, os 25 | import base64 26 | from optparse import OptionParser 27 | 28 | VERSION = '0.0.1' # keep in sync with setup.py 29 | 30 | UNITS = ['pt','px','in','mm'] 31 | PT2IN = 1.0/72.0 32 | IN2PT = 72.0 33 | MM2PT = 72.0/25.4 34 | PT2PX = 1.25 35 | PX2PT = 1.0/1.25 36 | 37 | relIRI_re = re.compile(r'url\(#(.*)\)') 38 | 39 | def get_unit_attr(value): 40 | # coordinate handling from http://www.w3.org/TR/SVG11/coords.html#Units 41 | units = None # default (user) 42 | for unit_name in UNITS: 43 | if value.endswith(unit_name): 44 | units = unit_name 45 | value = value[:-len(unit_name)] 46 | break 47 | val_float = float(value) # this will fail if units str not parsed 48 | return val_float, units 49 | 50 | def convert_to_pixels( val, units): 51 | if units == 'px' or units is None: 52 | val_px = val 53 | elif units == 'pt': 54 | val_px = val*PT2PX 55 | elif units == 'in': 56 | val_px = val*IN2PT*PT2PX 57 | elif units == 'mm': 58 | val_px = val*MM2PT*PT2PX 59 | else: 60 | raise ValueError('unsupport unit conversion to pixels: %s'%units) 61 | return val_px 62 | 63 | def fix_ids( elem, prefix, level=0 ): 64 | ns = '{http://www.w3.org/2000/svg}' 65 | 66 | if isinstance(elem.tag,basestring) and elem.tag.startswith(ns): 67 | 68 | tag = elem.tag[len(ns):] 69 | 70 | if 'id' in elem.attrib: 71 | elem.attrib['id'] = prefix + elem.attrib['id'] 72 | 73 | # fix references (See http://www.w3.org/TR/SVGTiny12/linking.html#IRIReference ) 74 | 75 | for attrib in elem.attrib.keys(): 76 | value = elem.attrib.get(attrib,None) 77 | 78 | if value is not None: 79 | 80 | if attrib.startswith('{http://www.w3.org/1999/xlink}'): 81 | relIRI = False 82 | else: 83 | relIRI = True 84 | 85 | if (not relIRI) and value.startswith('#'): # local IRI, change 86 | iri = value[1:] 87 | value = '#' + prefix + iri 88 | elem.attrib[attrib] = value 89 | elif relIRI: 90 | newvalue = re.sub( relIRI_re, r'url(#'+prefix+r'\1)', value) 91 | if newvalue != value: 92 | elem.attrib[attrib] = newvalue 93 | 94 | # Do same for children 95 | 96 | for child in elem: 97 | fix_ids(child,prefix,level=level+1) 98 | 99 | def export_images( elem, filename_fmt='image%03d', start_idx=1 ): 100 | """replace inline images with files""" 101 | ns = '{http://www.w3.org/2000/svg}' 102 | href = '{http://www.w3.org/1999/xlink}href' 103 | count = 0 104 | if isinstance(elem.tag,basestring) and elem.tag.startswith(ns): 105 | tag = elem.tag[len(ns):] 106 | if tag=='image': 107 | buf = etree.tostring(elem,pretty_print=True) 108 | im_data = elem.attrib[href] 109 | exts = ['png','jpeg'] 110 | found = False 111 | for ext in exts: 112 | prefix = 'data:image/'+ext+';base64,' 113 | if im_data.startswith(prefix): 114 | data_base64 = im_data[len(prefix):] 115 | found = True 116 | break 117 | if not found: 118 | raise NotImplementedError('image found but not supported') 119 | 120 | # decode data 121 | data = base64.b64decode(data_base64) 122 | 123 | # save data 124 | idx = start_idx + count 125 | fname = filename_fmt%idx + '.' + ext 126 | if os.path.exists(fname): 127 | raise RuntimeError('File exists: %r'%fname) 128 | with open(fname,mode='w') as fd: 129 | fd.write( data ) 130 | 131 | # replace element with link 132 | elem.attrib[href] = fname 133 | count += 1 134 | 135 | # Do same for children 136 | for child in elem: 137 | count += export_images(child, filename_fmt=filename_fmt, 138 | start_idx=(start_idx+count) ) 139 | return count 140 | 141 | header_str = """ 142 | 144 | 145 | """ 146 | 147 | # ------------------------------------------------------------------ 148 | class Document(object): 149 | def __init__(self): 150 | self._layout = None 151 | def setLayout(self,layout): 152 | self._layout = layout 153 | def save(self,fileobj,debug_boxes=False,**kwargs): 154 | if self._layout is None: 155 | raise ValueError('No layout, cannot save.') 156 | accum = LayoutAccumulator(**kwargs) 157 | self._layout.render(accum,debug_boxes=debug_boxes) 158 | if isinstance(fileobj,file): 159 | fd = fileobj 160 | close = False 161 | else: 162 | fd = open(fileobj,mode='w') 163 | close = True 164 | buf = accum.tostring(pretty_print=True) 165 | 166 | fd.write(header_str) 167 | fd.write( buf ) 168 | if close: 169 | fd.close() 170 | 171 | class SVGFileBase(object): 172 | def __init__(self,fname): 173 | self._fname = fname 174 | self._root = etree.parse(fname).getroot() 175 | if self._root.tag != '{http://www.w3.org/2000/svg}svg': 176 | raise ValueError('expected file to have root element ') 177 | 178 | height, height_units = get_unit_attr(self._root.get('height')) 179 | width, width_units = get_unit_attr(self._root.get('width')) 180 | self._width_px = convert_to_pixels( width, width_units) 181 | self._height_px = convert_to_pixels( height, height_units) 182 | self._orig_width_px = self._width_px 183 | self._orig_height_px = self._height_px 184 | self._coord = None # unassigned 185 | 186 | def get_root(self): 187 | return self._root 188 | 189 | def get_size(self,min_size=None,box_align=None,level=None): 190 | return Size(self._width_px,self._height_px) 191 | 192 | def _set_size(self,size): 193 | self._width_px = size.width 194 | self._height_px = size.height 195 | 196 | def _set_coord(self,coord): 197 | self._coord = coord 198 | 199 | def export_images(self,*args,**kwargs): 200 | export_images(self._root,*args,**kwargs) 201 | 202 | class SVGFile(SVGFileBase): 203 | def __str__(self): 204 | return 'SVGFile(%s)'%repr(self._fname) 205 | 206 | class SVGFileNoLayout(SVGFileBase): 207 | def __init__(self,fname,x=0,y=0): 208 | self._x_offset = x 209 | self._y_offset = y 210 | super(SVGFileNoLayout,self).__init__(fname) 211 | 212 | def _set_coord(self,coord): 213 | self._coord = (coord[0] + self._x_offset, 214 | coord[1] + self._y_offset ) 215 | 216 | def __str__(self): 217 | return 'SVGFileNoLayout(%s)'%repr(self._fname) 218 | 219 | class LayoutAccumulator(object): 220 | def __init__(self): 221 | self._svgfiles = [] 222 | self._svgfiles_no_layout = [] 223 | self._raw_elements = [] 224 | 225 | def add_svg_file(self,svgfile): 226 | assert isinstance(svgfile,SVGFile) 227 | if svgfile in self._svgfiles: 228 | raise ValueError('cannot accumulate SVGFile instance twice') 229 | self._svgfiles.append( svgfile ) 230 | 231 | def add_svg_file_no_layout(self,svgfile): 232 | assert isinstance(svgfile,SVGFileNoLayout) 233 | if svgfile in self._svgfiles_no_layout: 234 | raise ValueError('cannot accumulate SVGFileNoLayout instance twice') 235 | self._svgfiles_no_layout.append( svgfile ) 236 | 237 | def add_raw_element(self,elem): 238 | self._raw_elements.append( elem ) 239 | 240 | def tostring(self, **kwargs): 241 | root = self._make_finalized_root() 242 | return etree.tostring(root,**kwargs) 243 | 244 | def _set_size(self,size): 245 | self._size = size 246 | 247 | def _make_finalized_root(self): 248 | # get all required namespaces and prefixes 249 | NSMAP = {None : 'http://www.w3.org/2000/svg', 250 | 'sodipodi':'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 251 | } 252 | for svgfile in self._svgfiles: 253 | origelem = svgfile.get_root() 254 | for key,value in origelem.nsmap.iteritems(): 255 | if key in NSMAP: 256 | assert value == NSMAP[key] 257 | # Already in namespace dictionary 258 | continue 259 | elif key == 'svg': 260 | assert value == NSMAP[None] 261 | # svg is the default namespace - don't insert again. 262 | continue 263 | NSMAP[key] = value 264 | 265 | root = etree.Element('{http://www.w3.org/2000/svg}svg', 266 | nsmap=NSMAP) 267 | 268 | if 1: 269 | # inkscape hack 270 | root_defs = etree.SubElement(root,'{http://www.w3.org/2000/svg}defs') 271 | 272 | root.attrib['version']='1.1' 273 | fname_num = 0 274 | do_layout = True 275 | work_list=[] 276 | for svgfile in (self._svgfiles): 277 | work_list.append( (fname_num, do_layout, svgfile) ) 278 | fname_num += 1 279 | do_layout = False 280 | for svgfile in (self._svgfiles_no_layout): 281 | work_list.append( (fname_num, do_layout, svgfile) ) 282 | fname_num += 1 283 | for (fname_num, do_layout, svgfile) in work_list: 284 | origelem = svgfile.get_root() 285 | 286 | fix_id_prefix = 'id%d:'%fname_num 287 | elem = etree.SubElement(root,'{http://www.w3.org/2000/svg}g') 288 | elem.attrib['id'] = 'id%d'%fname_num 289 | 290 | elem_sz = svgfile.get_size() 291 | width_px = elem_sz.width 292 | height_px = elem_sz.height 293 | 294 | # copy svg contents into new group 295 | for child in origelem: 296 | if 1: 297 | # inkscape hacks 298 | if child.tag == '{http://www.w3.org/2000/svg}defs': 299 | # copy into root_defs, not into sub-group 300 | for subchild in child: 301 | fix_ids( subchild, fix_id_prefix ) 302 | root_defs.append( subchild ) 303 | continue 304 | elif child.tag == '{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}:namedview': 305 | # don't copy 306 | continue 307 | elif child.tag == '{http://www.w3.org/2000/svg}metadata': 308 | # don't copy 309 | continue 310 | elem.append(child) 311 | 312 | fix_ids( elem, fix_id_prefix ) 313 | 314 | translate_x = svgfile._coord[0] 315 | translate_y = svgfile._coord[1] 316 | if do_layout: 317 | if svgfile._orig_width_px != width_px: 318 | raise NotImplementedError('rescaling width not implemented ' 319 | '(hint: set alignment on file %s)'%( 320 | svgfile,)) 321 | if svgfile._orig_height_px != height_px: 322 | raise NotImplementedError('rescaling height not implemented ' 323 | '(hint: set alignment on file %s)'%( 324 | svgfile,)) 325 | orig_viewBox = origelem.get('viewBox') 326 | if orig_viewBox is not None: 327 | # split by comma or whitespace 328 | vb_tup = orig_viewBox.split(',') 329 | vb_tup = [c.strip() for c in vb_tup] 330 | if len(vb_tup)==1: 331 | # not separated by commas 332 | vb_tup = orig_viewBox.split() 333 | assert len(vb_tup)==4 334 | vb_tup = [float(v) for v in vb_tup] 335 | vbminx, vbminy, vbwidth, vbheight = vb_tup 336 | sx = width_px / vbwidth 337 | sy = height_px / vbheight 338 | tx = translate_x - vbminx 339 | ty = translate_y - vbminy 340 | elem.attrib['transform'] = 'matrix(%s,0,0,%s,%s,%s)'%( 341 | sx,sy,tx,ty) 342 | else: 343 | elem.attrib['transform'] = 'translate(%s,%s)'%( 344 | translate_x, translate_y) 345 | root.append( elem ) 346 | for elem in self._raw_elements: 347 | root.append(elem) 348 | 349 | root.attrib["width"] = repr(self._size.width) 350 | root.attrib["height"] = repr(self._size.height) 351 | 352 | return root 353 | 354 | # ------------------------------------------------------------------ 355 | class Size(object): 356 | def __init__(self, width=0, height=0): 357 | self.width=width 358 | self.height=height 359 | 360 | # directions for BoxLayout 361 | LeftToRight = 'LeftToRight' 362 | RightToLeft = 'RightToLeft' 363 | TopToBottom = 'TopToBottom' 364 | BottomToTop = 'BottomToTop' 365 | 366 | # alignment values 367 | AlignLeft = 0x01 368 | AlignRight = 0x02 369 | AlignHCenter = 0x04 370 | 371 | AlignTop = 0x020 372 | AlignBottom = 0x040 373 | AlignVCenter = 0x080 374 | 375 | AlignCenter = AlignHCenter | AlignVCenter 376 | 377 | class Layout(object): 378 | def __init__(self, parent=None): 379 | if parent is not None: 380 | raise NotImplementedError('') 381 | 382 | class BoxLayout(Layout): 383 | def __init__(self, direction, parent=None): 384 | super(BoxLayout,self).__init__(parent=parent) 385 | self._direction = direction 386 | self._items = [] 387 | self._contents_margins = 0 # around edge of box 388 | self._spacing = 0 # between items in box 389 | self._coord = (0,0) # default 390 | self._size = None # uncalculated 391 | 392 | def _set_coord(self,coord): 393 | self._coord = coord 394 | 395 | def render(self,accum, min_size=None, level=0, debug_boxes=0): 396 | size = self.get_size(min_size=min_size) 397 | if level==0: 398 | # set document size if top level 399 | accum._set_size(size) 400 | if debug_boxes>0: 401 | # draw black line around BoxLayout element 402 | debug_box = etree.Element('{http://www.w3.org/2000/svg}rect') 403 | debug_box.attrib['style']=( 404 | 'fill: none; stroke: black; stroke-width: 2.000000;') 405 | sz=size 406 | debug_box.attrib['x']=repr(self._coord[0]) 407 | debug_box.attrib['y']=repr(self._coord[1]) 408 | debug_box.attrib['width']=repr(sz.width) 409 | debug_box.attrib['height']=repr(sz.height) 410 | accum.add_raw_element(debug_box) 411 | 412 | for (item,stretch,alignment,xml) in self._items: 413 | if isinstance( item, SVGFile ): 414 | accum.add_svg_file(item) 415 | 416 | if debug_boxes>0: 417 | # draw red line around SVG file 418 | debug_box= etree.Element('{http://www.w3.org/2000/svg}rect') 419 | debug_box.attrib['style']=( 420 | 'fill: none; stroke: red; stroke-width: 1.000000;') 421 | sz=item.get_size() 422 | debug_box.attrib['x']=repr(item._coord[0]) 423 | debug_box.attrib['y']=repr(item._coord[1]) 424 | debug_box.attrib['width']=repr(sz.width) 425 | debug_box.attrib['height']=repr(sz.height) 426 | accum.add_raw_element(debug_box) 427 | elif isinstance( item, SVGFileNoLayout ): 428 | accum.add_svg_file_no_layout(item) 429 | 430 | if debug_boxes>0: 431 | # draw green line around SVG file 432 | debug_box= etree.Element('{http://www.w3.org/2000/svg}rect') 433 | debug_box.attrib['style']=( 434 | 'fill: none; stroke: green; stroke-width: 1.000000;') 435 | sz=item.get_size() 436 | debug_box.attrib['x']=repr(item._coord[0]) 437 | debug_box.attrib['y']=repr(item._coord[1]) 438 | debug_box.attrib['width']=repr(sz.width) 439 | debug_box.attrib['height']=repr(sz.height) 440 | accum.add_raw_element(debug_box) 441 | 442 | elif isinstance( item, BoxLayout ): 443 | item.render( accum, min_size=item._size, level=level+1, 444 | debug_boxes=debug_boxes) 445 | else: 446 | raise NotImplementedError( 447 | "don't know how to accumulate item %s"%item) 448 | 449 | if xml is not None: 450 | extra = etree.Element('{http://www.w3.org/2000/svg}g') 451 | extra.attrib['transform'] = 'translate(%s,%s)'%( 452 | repr(item._coord[0]),repr(item._coord[1])) 453 | extra.append(xml) 454 | accum.add_raw_element(extra) 455 | 456 | def get_size(self, min_size=None, box_align=0, level=0 ): 457 | cum_dim = 0 # size along layout direction 458 | max_orth_dim = 0 # size along other direction 459 | 460 | if min_size is None: 461 | min_size = Size(0,0) 462 | 463 | # Step 1: calculate required size along self._direction 464 | if self._direction in [LeftToRight, RightToLeft]: 465 | max_orth_dim = min_size.height 466 | dim_min_size = Size(width=0, 467 | height=max_orth_dim) 468 | else: 469 | max_orth_dim = min_size.width 470 | dim_min_size = Size(width=max_orth_dim, 471 | height=0) 472 | 473 | cum_dim += self._contents_margins # first margin 474 | item_sizes = [] 475 | for item_number,(item,stretch,alignment,xml) in enumerate(self._items): 476 | if isinstance(item,SVGFileNoLayout): 477 | item_size = Size(0,0) 478 | else: 479 | item_size = item.get_size(min_size=dim_min_size, box_align=alignment,level=level+1) 480 | item_sizes.append( item_size ) 481 | 482 | if isinstance(item,SVGFileNoLayout): 483 | # no layout for this file 484 | continue 485 | 486 | if self._direction in [LeftToRight, RightToLeft]: 487 | cum_dim += item_size.width 488 | max_orth_dim = max(max_orth_dim,item_size.height) 489 | else: 490 | cum_dim += item_size.height 491 | max_orth_dim = max(max_orth_dim,item_size.width) 492 | 493 | if (item_number+1) < len(self._items): 494 | cum_dim += self._spacing # space between elements 495 | cum_dim += self._contents_margins # last margin 496 | orth_dim = max_orth_dim # value without adding margins 497 | max_orth_dim += 2*self._contents_margins # margins 498 | 499 | # --------------------------------- 500 | 501 | # Step 2: another pass in which expansion takes place 502 | total_stretch = 0 503 | for item,stretch,alignment,xml in self._items: 504 | total_stretch += stretch 505 | if (self._direction in [LeftToRight, RightToLeft]): 506 | dim_unfilled_length = max(0,min_size.width - cum_dim) 507 | else: 508 | dim_unfilled_length = max(0,min_size.height - cum_dim) 509 | 510 | stretch_hack = False 511 | if dim_unfilled_length > 0: 512 | if total_stretch == 0: 513 | # BoxLayout in which stretch is 0, but unfilled space 514 | # exists. 515 | 516 | # XXX TODO: what is Qt policy in this case? 517 | stretch_hack = True 518 | stretch_inc = 0 519 | else: 520 | stretch_inc = dim_unfilled_length / float(total_stretch) 521 | else: 522 | stretch_inc = 0 523 | 524 | cum_dim = 0 # size along layout direction 525 | cum_dim += self._contents_margins # first margin 526 | is_last_item = False 527 | for i,(_item,old_item_size) in enumerate(zip(self._items,item_sizes)): 528 | if (i+1) >= len(self._items): 529 | is_last_item=True 530 | (item,stretch,alignment,xml) = _item 531 | if (self._direction in [LeftToRight, RightToLeft]): 532 | new_dim_length = old_item_size.width + stretch*stretch_inc 533 | if stretch_hack and is_last_item: 534 | new_dim_length = old_item_size.width + dim_unfilled_length 535 | new_item_size = Size( new_dim_length, orth_dim ) 536 | else: 537 | new_dim_length = old_item_size.height + stretch*stretch_inc 538 | if stretch_hack and is_last_item: 539 | new_dim_length = old_item_size.width + dim_unfilled_length 540 | new_item_size = Size( orth_dim, new_dim_length ) 541 | 542 | if isinstance(item,SVGFileNoLayout): 543 | item_size = Size(0,0) 544 | else: 545 | item_size = item.get_size(min_size=new_item_size, box_align=alignment,level=level+1) 546 | if self._direction == LeftToRight: 547 | child_box_coord = (cum_dim, self._contents_margins) 548 | elif self._direction == TopToBottom: 549 | child_box_coord = (self._contents_margins, cum_dim) 550 | else: 551 | raise NotImplementedError( 552 | 'direction %s not implemented'%self._direction) 553 | child_box_coord = (child_box_coord[0] + self._coord[0], 554 | child_box_coord[1] + self._coord[1]) 555 | child_box_size = new_item_size 556 | 557 | item_pos, final_item_size = self._calc_box( child_box_coord, child_box_size, 558 | item_size, 559 | alignment ) 560 | item._set_coord( item_pos ) 561 | item._set_size( final_item_size ) 562 | 563 | if self._direction in [LeftToRight, RightToLeft]: 564 | # Use requested item size so ill behaved item doesn't 565 | # screw up layout. 566 | cum_dim += new_item_size.width 567 | else: 568 | # Use requested item size so ill behaved item doesn't 569 | # screw up layout. 570 | cum_dim += new_item_size.height 571 | 572 | if not is_last_item: 573 | cum_dim += self._spacing # space between elements 574 | cum_dim += self._contents_margins # last margin 575 | 576 | # --------------------------------- 577 | 578 | # Step 3: calculate coordinates of each item 579 | 580 | if self._direction in [LeftToRight, RightToLeft]: 581 | size = Size(cum_dim, max_orth_dim) 582 | else: 583 | size = Size(max_orth_dim, cum_dim) 584 | 585 | self._size = size 586 | return size 587 | 588 | def _calc_box(self, in_pos, in_sz, item_sz, alignment): 589 | if (AlignLeft & alignment): 590 | left = in_pos[0] 591 | width = item_sz.width 592 | elif (AlignRight & alignment): 593 | left = in_pos[0]+in_sz.width-item_sz.width 594 | width = item_sz.width 595 | elif (AlignHCenter & alignment): 596 | left = in_pos[0]+0.5*(in_sz.width-item_sz.width) 597 | width = item_sz.width 598 | else: 599 | # expand 600 | left = in_pos[0] 601 | width = in_sz.width 602 | 603 | if (AlignTop & alignment): 604 | top = in_pos[1] 605 | height = item_sz.height 606 | elif (AlignBottom & alignment): 607 | top = in_pos[1]+in_sz.height-item_sz.height 608 | height = item_sz.height 609 | elif (AlignVCenter & alignment): 610 | top = in_pos[1]+0.5*(in_sz.height-item_sz.height) 611 | height = item_sz.height 612 | else: 613 | # expand 614 | top = in_pos[1] 615 | height = in_sz.height 616 | 617 | pos = (left,top) 618 | size = Size(width,height) 619 | return pos,size 620 | 621 | def _set_size(self, size): 622 | self._size = size 623 | 624 | def setSpacing(self,spacing): 625 | self._spacing = spacing 626 | 627 | def addSVG(self, svg_file, stretch=0, alignment=0, xml=None): 628 | if not isinstance(svg_file,SVGFile): 629 | svg_file = SVGFile(svg_file) 630 | if xml is not None: 631 | xml = etree.XML(xml) 632 | self._items.append((svg_file,stretch,alignment,xml)) 633 | 634 | def addSVGNoLayout(self, svg_file, x=0, y=0, xml=None): 635 | if not isinstance(svg_file,SVGFileNoLayout): 636 | svg_file = SVGFileNoLayout(svg_file,x=x,y=y) 637 | stretch=0 638 | alignment=0 639 | if xml is not None: 640 | xml = etree.XML(xml) 641 | self._items.append((svg_file,stretch,alignment,xml)) 642 | 643 | def addLayout(self, layout, stretch=0): 644 | assert isinstance(layout,Layout) 645 | alignment=0 # always expand a layout 646 | xml=None 647 | self._items.append((layout,stretch,alignment,xml)) 648 | 649 | class HBoxLayout(BoxLayout): 650 | def __init__(self, parent=None): 651 | super(HBoxLayout,self).__init__(LeftToRight,parent=parent) 652 | 653 | class VBoxLayout(BoxLayout): 654 | def __init__(self, parent=None): 655 | super(VBoxLayout,self).__init__(TopToBottom,parent=parent) 656 | 657 | # ------------------------------------------------------------------ 658 | 659 | def main(): 660 | usage = '''%prog FILE1 [FILE2] [...] [options] 661 | 662 | concatenate SVG files 663 | 664 | This will concatenate FILE1, FILE2, ... to a new svg file printed to 665 | stdout. 666 | 667 | ''' 668 | 669 | parser = OptionParser(usage, version=VERSION) 670 | parser.add_option("--margin",type='str', 671 | help='size of margin (in any units, px default)', 672 | default=None) 673 | parser.add_option("--direction",type='str', 674 | default='vertical', 675 | help='horizontal or vertical (or h or v)') 676 | (options, args) = parser.parse_args() 677 | fnames = args 678 | 679 | if options.direction.lower().startswith('v'): 680 | direction = 'vertical' 681 | elif options.direction.lower().startswith('h'): 682 | direction = 'horizontal' 683 | else: 684 | raise ValueError('unknown direction %s'%options.direction) 685 | 686 | if options.margin is not None: 687 | margin_px = convert_to_pixels(*get_unit_attr(options.margin)) 688 | else: 689 | margin_px = 0 690 | 691 | if 0: 692 | fd = open('tmp.svg',mode='w') 693 | else: 694 | fd = sys.stdout 695 | 696 | doc = Document() 697 | if direction == 'vertical': 698 | layout = VBoxLayout() 699 | elif direction == 'horizontal': 700 | layout = HBoxLayout() 701 | 702 | for fname in fnames: 703 | layout.addSVG(fname,alignment=AlignCenter) 704 | 705 | layout.setSpacing(margin_px) 706 | doc.setLayout(layout) 707 | doc.save( fd ) 708 | 709 | if __name__=='__main__': 710 | main() 711 | -------------------------------------------------------------------------------- /pyggplot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2015, Florian Finkernagel. All rights reserved. 2 | 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are 5 | # met: 6 | 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | 15 | # * Neither the name of the Andrew Straw nor the names of its 16 | # contributors may be used to endorse or promote products derived 17 | # from this software without specific prior written permission. 18 | 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | """A wrapper around ggplot2 ( http://had.co.nz/ggplot2/ ) and 32 | plotnine (https://plotnine.readthedocs.io/en/stable/index.html ) 33 | 34 | Takes a pandas.DataFrame object, then add layers with the various add_xyz 35 | functions (e.g. add_scatter). 36 | 37 | Referr to the ggplot/plotnine documentation about the layers (geoms), and simply 38 | replace geom_* with add_*. 39 | See http://docs.ggplot2.org/0.9.3.1/index.html or 40 | https://plotnine.readthedocs.io/en/stable/index.html 41 | 42 | You do not need to seperate aesthetics from values - the wrapper 43 | will treat a parameter as value if and only if it is not a column name. 44 | (so y = 0 is a value, color = 'blue 'is a value - except if you have a column 45 | 'blue', then it's a column!. And y = 'value' doesn't work, but that seems to be a ggplot issue). 46 | 47 | When the DataFrame is passed to the plotting library: 48 | - row indices are truned into columns with 'reset_index' 49 | - multi level column indices are flattend by concatenating them with ' ' 50 | -> (X, 'mean) becomes 'x mean' 51 | 52 | R Error messages are not great - most of them translate to 'one or more columns were not found', 53 | but they can appear as a lot of different actual messages such as 54 | - argument "env" is missing, with no defalut 55 | - object 'y' not found 56 | - object 'dat_0' not found 57 | - requires the follewing missing aesthetics: x 58 | - non numeric argument to binary operator 59 | without actually quite pointing at what is strictly the offending value. 60 | Also, the error appears when rendering (or printing in ipython notebook), 61 | not when adding the layer. 62 | """ 63 | #from .plot_r import Plot, plot_heatmap, multiplot, MultiPagePlot, convert_dataframe_to_r 64 | from .base import _PlotBase 65 | #from . import plot_nine 66 | from .plot_nine import Plot, Expression, Scalar 67 | 68 | all = [Plot, Expression, Scalar] 69 | -------------------------------------------------------------------------------- /pyggplot/base.py: -------------------------------------------------------------------------------- 1 | class _PlotBase(object): 2 | pass 3 | -------------------------------------------------------------------------------- /pyggplot/plot_nine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import pandas as pd 4 | import numpy as np 5 | import itertools 6 | from .base import _PlotBase 7 | #try: 8 | #import exptools 9 | #exptools.load_software('palettable') 10 | #exptools.load_software('descartes') 11 | #exptools.load_software('mizani') 12 | #exptools.load_software('patsy') 13 | #exptools.load_software('plotnine') 14 | 15 | #except ImportError: 16 | #pass 17 | import matplotlib 18 | matplotlib.use('agg') 19 | import plotnine as p9 20 | from plotnine import stats 21 | 22 | 23 | class Expression: 24 | def __init__(self, expr_str): 25 | self.expr_str = expr_str 26 | 27 | 28 | class Scalar: 29 | def __init__(self, scalar_str): 30 | self.scalar_str = scalar_str 31 | 32 | 33 | class Plot(_PlotBase): 34 | def __init__(self, dataframe): 35 | self.dataframe = self._prep_dataframe(dataframe) 36 | self.ipython_plot_width = 600 37 | self.ipython_plot_height = 600 38 | self.plot = p9.ggplot(self.dataframe) 39 | self._add_geoms() 40 | self._add_scales_and_cords_and_facets_and_themes() 41 | self._add_positions_and_stats() 42 | 43 | def __add__(self, other): 44 | if hasattr(other, '__radd__'): 45 | # let's assume that it's a plotnine object that knows how to radd it 46 | # self to our plot 47 | self.plot = self.plot + other 48 | return self 49 | 50 | 51 | def _prep_dataframe(self, df): 52 | """prepare the dataframe by making sure it's a pandas dataframe, 53 | has no multi index columns, has no index etc 54 | """ 55 | if 'pydataframe.dataframe.DataFrame' in str(type(df)): 56 | df = self._convert_pydataframe(df) 57 | elif isinstance(df, dict): 58 | df = pd.DataFrame(df) 59 | elif isinstance(df, pd.Series): 60 | df = pd.DataFrame(df) 61 | if isinstance(df.columns, pd.MultiIndex): 62 | df.columns = [' '.join(col).strip() for col in df.columns.values] 63 | df = df.reset_index() 64 | return df 65 | 66 | def _convert_pydataframe(self, pdf): 67 | """Compability shim for still being able to use old pydataframes with the new pandas interface""" 68 | d = {} 69 | for column in pdf.columns_ordered: 70 | o = pdf.gcv(column) 71 | if 'pydataframe.factors.Factor' in str(type(o)): 72 | d[column] = pd.Series( 73 | pd.Categorical(o.as_levels(), categories=o.levels)) 74 | else: 75 | d[column] = o 76 | return pd.DataFrame(d) 77 | 78 | def _repr_png_(self, width=None, height=None): 79 | """Show the plot in the ipython notebook (ie. return png formated image data)""" 80 | if width is None: 81 | width = self.ipython_plot_width 82 | height = self.ipython_plot_height 83 | try: 84 | handle, name = tempfile.mkstemp( 85 | suffix=".png" 86 | ) # mac os for some reason would not read back again from a named tempfile. 87 | os.close(handle) 88 | self.plot.save( 89 | name, 90 | width=width / 72., 91 | height=height / 72., 92 | dpi=72, 93 | verbose=False) 94 | tf = open(name, "rb") 95 | result = tf.read() 96 | tf.close() 97 | return result 98 | finally: 99 | os.unlink(name) 100 | 101 | def _repr_svg_(self, width=None, height=None): 102 | """Show the plot in the ipython notebook (ie. return svg formated image data)""" 103 | if width is None: 104 | width = self.ipython_plot_width / 150. * 72 105 | height = self.ipython_plot_height / 150. * 72 106 | try: 107 | handle, name = tempfile.mkstemp( 108 | suffix=".svg" 109 | ) # mac os for some reason would not read back again from a named tempfile. 110 | os.close(handle) 111 | self.plot.save( 112 | name, 113 | width=width / 72., 114 | height=height / 72., 115 | dpi=72, 116 | verbose=False) 117 | tf = open(name, "rb") 118 | result = tf.read().decode('utf-8') 119 | tf.close() 120 | # newer jupyters need the height attribute 121 | # otherwise the iframe is only one line high and 122 | # the figure tiny 123 | result = result.replace("viewBox=", 124 | 'height=\'%i\' viewBox=' % (height)) 125 | return result, {"isolated": True} 126 | finally: 127 | os.unlink(name) 128 | 129 | #def geom_scatter(self, x, y): 130 | #self.plot += p9.geom_point(p9.aes(x, y)) 131 | 132 | def _add(self, geom_class, args, kwargs): 133 | """The generic method to add a geom to the ggplot. 134 | You need to call add_xyz (see _add_geom_methods for a list, with each variable mapping 135 | being one argument) with the respectivly required parameters (see ggplot documentation). 136 | You may optionally pass in an argument called data, which will replace the plot-global dataframe 137 | for this particular geom 138 | """ 139 | 140 | if 'data' in kwargs: 141 | data = self._prep_dataframe(kwargs['data']) 142 | else: 143 | data = None 144 | if 'stat' in kwargs: 145 | stat = kwargs['stat'] 146 | else: 147 | stat = geom_class.DEFAULT_PARAMS['stat'] 148 | if isinstance(stat, str): 149 | stat = getattr( 150 | p9.stats, 'stat_' + stat 151 | if not stat.startswith('stat_') else stat) 152 | mapping = {} 153 | out_kwargs = {} 154 | all_defined_mappings = list( 155 | stat.REQUIRED_AES) + list(geom_class.REQUIRED_AES) + list( 156 | geom_class.DEFAULT_AES) + ['group'] # + list(geom_class.DEFAULT_PARAMS) 157 | if 'x' in geom_class.REQUIRED_AES: 158 | if len(args) > 0: 159 | kwargs['x'] = args[0] 160 | if 'y' in geom_class.REQUIRED_AES or 'y' in stat.REQUIRED_AES: 161 | if len(args) > 1: 162 | kwargs['y'] = args[1] 163 | if len(args) > 2: 164 | raise ValueError( 165 | "We only accept x&y by position, all other args need to be named" 166 | ) 167 | else: 168 | if len(args) > 1: 169 | raise ValueError( 170 | "We only accept x by position, all other args need to be named" 171 | ) 172 | elif 'xmin' and 'ymin' in geom_class.REQUIRED_AES: 173 | if len(args) > 0: 174 | kwargs['xmin'] = args[0] 175 | if len(args) > 1: 176 | kwargs['xmax'] = args[1] 177 | if len(args) > 2: 178 | kwargs['ymin'] = args[2] 179 | if len(args) > 3: 180 | kwargs['ymax'] = args[3] 181 | elif len(args) > 4: 182 | raise ValueError( 183 | "We only accept xmin,xmax,ymin,ymax by position, all other args need to be named" 184 | ) 185 | 186 | for a, b in kwargs.items(): 187 | if a in all_defined_mappings: 188 | # if it is an expression, keep it that way 189 | # if it's a single value, treat it as a scalar 190 | # except if it looks like an expression (ie. has () 191 | is_kwarg = False 192 | if isinstance(b, Expression): 193 | b = b.expr_str 194 | is_kwarg = True 195 | elif isinstance(b, Scalar): 196 | b = b.scalar_str 197 | is_kwarg = True 198 | elif (((data is not None and b not in data.columns) or 199 | (data is None and b not in self.dataframe.columns)) 200 | and not '(' in str( 201 | b) # so a true scalar, not a calculated expression 202 | ): 203 | b = b # which will tell it to treat it as a scalar! 204 | is_kwarg = True 205 | if not is_kwarg: 206 | mapping[a] = b 207 | else: 208 | out_kwargs[a] = b 209 | 210 | #mapping.update({x: kwargs[x] for x in kwargs if x in all_defined_mappings}) 211 | 212 | out_kwargs['data'] = data 213 | for a in geom_class.DEFAULT_PARAMS: 214 | if a in kwargs: 215 | out_kwargs[a] = kwargs[a] 216 | 217 | self.plot += geom_class(mapping=p9.aes(**mapping), **out_kwargs) 218 | return self 219 | 220 | def _add_geoms(self): 221 | # allow aliases 222 | name_to_geom = {} 223 | for name in dir(p9): 224 | if name.startswith('geom_'): 225 | geom = getattr(p9, name) 226 | name_to_geom[name] = geom 227 | name_to_geom['geom_scatter'] = name_to_geom['geom_point'] 228 | for name, geom in name_to_geom.items(): 229 | 230 | def define(geom): 231 | def do_add(*args, **kwargs): 232 | return self._add(geom_class=geom, args=args, kwargs=kwargs) 233 | 234 | do_add.__doc__ = geom.__doc__ 235 | return do_add 236 | 237 | method_name = 'add_' + name[5:] 238 | if not hasattr(self, method_name): 239 | setattr(self, method_name, define(geom)) 240 | 241 | return self 242 | 243 | def _add_scales_and_cords_and_facets_and_themes(self): 244 | for name in dir(p9): 245 | if name.startswith('scale_') or name.startswith( 246 | 'coord_') or name.startswith('facet') or name.startswith( 247 | 'theme'): 248 | method_name = name 249 | if not hasattr(self, method_name): 250 | scale = getattr(p9, name) 251 | 252 | def define(scale): 253 | def add_(*args, **kwargs): 254 | self.plot += scale(*args, **kwargs) 255 | return self 256 | 257 | add_.__doc__ = scale.__doc__ 258 | return add_ 259 | 260 | setattr(self, method_name, define(scale)) 261 | 262 | def _add_positions_and_stats(self): 263 | for name in dir(p9): 264 | if name.startswith('stat_') or name.startswith('position'): 265 | setattr(self, name, getattr(p9, name)) 266 | 267 | def title(self, text): 268 | """Set plot title""" 269 | self.plot += p9.ggtitle(text) 270 | 271 | def set_title(self, text): 272 | """Set plot title""" 273 | self.title(text) 274 | 275 | def facet(self, *args, **kwargs): 276 | """Compability to old calling style""" 277 | if 'free_y' in kwargs['scales']: 278 | self.plot += p9.theme(subplots_adjust={'wspace':0.2}) 279 | return self.facet_wrap(*args, **kwargs) 280 | 281 | def add_jitter(self, x, y, jitter_x=True, jitter_y=True, **kwargs): 282 | # an api changed in ggplot necessitates this - jitter_x and jitter_y have been replaced with position_jitter(width, height)... 283 | kwargs['position'] = self.position_jitter( 284 | width=0.4 if jitter_x is True else float(jitter_x), 285 | height=0.4 if jitter_y is True else float(jitter_y), 286 | ) 287 | self.add_point(x, y, **kwargs) 288 | 289 | def add_cummulative(self, 290 | x_column, 291 | ascending=True, 292 | percent=False, 293 | percentile=1.0): 294 | """Add a line showing cumulative % of data <= x. 295 | if you specify a percentile, all data at the extreme range is dropped 296 | 297 | 298 | """ 299 | total = 0 300 | current = 0 301 | column_data = self.dataframe[x_column].copy() # explicit copy! 302 | column_data = column_data[~np.isnan(column_data)] 303 | column_data = np.sort(column_data) 304 | total = float(len(column_data)) 305 | real_total = total 306 | if not ascending: 307 | column_data = column_data[::-1] # numpy.reverse(column_data) 308 | if percentile != 1.0: 309 | if ascending: 310 | maximum = np.max(column_data) 311 | else: 312 | maximum = np.min(column_data) 313 | total = float(total * percentile) 314 | if total > 0: 315 | column_data = column_data[:total] 316 | offset = real_total - total 317 | else: 318 | column_data = column_data[total:] 319 | offset = 2 * abs(total) 320 | else: 321 | offset = 0 322 | x_values = [] 323 | y_values = [] 324 | if percent: 325 | current = 100.0 326 | else: 327 | current = total 328 | for value, group in itertools.groupby(column_data): 329 | x_values.append(value) 330 | y_values.append(current + offset) 331 | if percent: 332 | current -= (len(list(group)) / total) 333 | else: 334 | current -= (len(list(group))) 335 | # y_values.append(current) 336 | data = pd.DataFrame({ 337 | x_column: x_values, 338 | ("%" if percent else '#') + ' <=': y_values 339 | }) 340 | if percentile > 0: 341 | self.scale_x_continuous( 342 | limits=[0, real_total if not percent else 100]) 343 | self.add_line(x_column, ("%" if percent else '#') + ' <=', data=data) 344 | if percentile != 1.0: 345 | self.set_title('showing only %.2f percentile, extreme was %.2f' % 346 | (percentile, maximum)) 347 | return self 348 | 349 | def add_heatmap(self, 350 | x_column, 351 | y_column, 352 | fill, 353 | low="red", 354 | mid=None, 355 | high="blue", 356 | midpoint=0, 357 | guide_legend=None, 358 | scale_args=None): 359 | self.add_tile(x_column, y_column, fill=fill) 360 | if mid is None: 361 | self.scale_fill_gradient(low=low, high=high, **scale_args) 362 | else: 363 | self.scale_fill_gradient2( 364 | low=low, mid=mid, high=high, midpoint=midpoint, **scale_args) 365 | 366 | def add_alternating_background(self, 367 | x_column, 368 | fill_1="#EEEEEE", 369 | fill_2="#FFFFFF", 370 | vertical=False, 371 | alpha=0.5, 372 | log_y_scale=False, 373 | facet_column=None): 374 | """Add an alternating background to a categorial (x-axis) plot. 375 | """ 376 | self.scale_x_discrete() 377 | if log_y_scale: 378 | self._expected_y_scale = 'log' 379 | else: 380 | self._expected_y_scale = 'normal' 381 | if facet_column is None: 382 | sub_frames = [(False, self.dataframe)] 383 | else: 384 | sub_frames = self.dataframe.groupby(facet_column) 385 | 386 | for facet_value, facet_df in sub_frames: 387 | no_of_x_values = len(facet_df[x_column].unique()) 388 | df_rect = pd.DataFrame({ 389 | 'xmin': 390 | np.array(range(no_of_x_values)) - .5 + 1, 391 | 'xmax': 392 | np.array(range(no_of_x_values)) + .5 + 1, 393 | 'ymin': 394 | -np.inf if not log_y_scale else 0, 395 | 'ymax': 396 | np.inf, 397 | 'fill': 398 | ([fill_1, fill_2] * (no_of_x_values // 2 + 1))[:no_of_x_values] 399 | }) 400 | if facet_value is not False: 401 | df_rect.insert(0, facet_column, facet_value) 402 | #df_rect.insert(0, 'alpha', alpha) 403 | left = df_rect[df_rect.fill == fill_1] 404 | right = df_rect[df_rect.fill == fill_2] 405 | if not vertical: 406 | if len(left): 407 | self.add_rect( 408 | 'xmin', 409 | 'xmax', 410 | 'ymin', 411 | 'ymax', 412 | fill='fill', 413 | data=left, 414 | alpha=alpha) 415 | if len(right): 416 | self.add_rect( 417 | 'xmin', 418 | 'xmax', 419 | 'ymin', 420 | 'ymax', 421 | fill='fill', 422 | data=right, 423 | alpha=alpha) 424 | else: 425 | if len(left): 426 | self.add_rect( 427 | 'ymin', 428 | 'ymax', 429 | 'xmin', 430 | 'xmax', 431 | fill='fill', 432 | data=left, 433 | alpha=alpha) 434 | if len(right): 435 | self.add_rect( 436 | 'ymin', 437 | 'ymax', 438 | 'xmin', 439 | 'xmax', 440 | fill='fill', 441 | data=right, 442 | alpha=alpha) 443 | self.scale_fill_identity() 444 | self.scale_alpha_identity() 445 | return self 446 | 447 | def turn_x_axis_labels(self, 448 | angle=90, 449 | hjust='center', 450 | vjust='top', 451 | size=None, 452 | color=None): 453 | return self.turn_axis_labels('axis_text_x', angle, hjust, vjust, size, 454 | color) 455 | 456 | def turn_y_axis_labels(self, 457 | angle=90, 458 | hjust='hjust', 459 | vjust='center', 460 | size=None, 461 | color=None): 462 | return self.turn_axis_labels('axis_text_y', angle, hjust, vjust, size, 463 | color) 464 | 465 | def _change_theme(self, what, t): 466 | if self.plot.theme is None: 467 | self.theme_grey() 468 | self.plot.theme += p9.theme(**{what: t}) 469 | return self 470 | 471 | def turn_axis_labels(self, ax, angle, hjust, vjust, size, color): 472 | t = p9.themes.element_text( 473 | rotation=angle, ha=hjust, va=vjust, size=size, color=color) 474 | return self._change_theme(ax, t) 475 | 476 | def hide_background(self): 477 | return self._change_theme('panel_background', p9.element_blank()) 478 | 479 | def hide_y_axis_labels(self): 480 | return self._change_theme('axis_text_y', p9.element_blank()) 481 | 482 | def hide_x_axis_labels(self): 483 | return self._change_theme('axis_text_x', p9.element_blank()) 484 | 485 | def hide_axis_ticks(self): 486 | return self._change_theme('axis_ticks', p9.element_blank()) 487 | 488 | def hide_axis_ticks_x(self): 489 | return self._change_theme('axis_ticks_major_x', p9.element_blank()) 490 | 491 | def hide_axis_ticks_y(self): 492 | return self._change_theme('axis_ticks_major_y', p9.element_blank()) 493 | 494 | def hide_y_axis_title(self): 495 | return self._change_theme('axis_title_y', p9.element_blank()) 496 | 497 | def hide_x_axis_title(self): 498 | return self._change_theme('axis_title_x', p9.element_blank()) 499 | 500 | def hide_facet_labels(self): 501 | self._change_theme('strip_background', p9.element_blank()) 502 | return self._change_theme('strip_text_x', p9.element_blank()) 503 | 504 | def hide_legend_key(self): 505 | raise ValueError("plotnine doesn't do 'hide_legend' - pass show_legend=False to the geoms instead") 506 | 507 | _many_cat_colors = [ 508 | "#1C86EE", 509 | "#E31A1C", # red 510 | "#008B00", 511 | "#6A3D9A", # purple 512 | "#FF7F00", # orange 513 | "#4D4D4D", 514 | "#FFD700", 515 | "#7EC0EE", 516 | "#FB9A99", # lt pink 517 | "#90EE90", 518 | # "#CAB2D6", # lt purple 519 | "#0000FF", 520 | "#FDBF6F", # lt orange 521 | "#B3B3B3", 522 | "EEE685", 523 | "#B03060", 524 | "#FF83FA", 525 | "#FF1493", 526 | "#0000FF", 527 | "#36648B", 528 | "#00CED1", 529 | "#00FF00", 530 | "#8B8B00", 531 | "#CDCD00", 532 | "#8B4500", 533 | "#A52A2A" 534 | ] 535 | 536 | def scale_fill_many_categories(self, offset=0, **kwargs): 537 | self.scale_fill_manual((self._many_cat_colors + self._many_cat_colors 538 | )[offset:offset + len(self._many_cat_colors)], **kwargs) 539 | return self 540 | 541 | def scale_color_many_categories(self, offset=0, **kwargs): 542 | self.scale_color_manual((self._many_cat_colors + self._many_cat_colors 543 | )[offset:offset + len(self._many_cat_colors)], **kwargs) 544 | return self 545 | 546 | def render(self, 547 | output_filename, 548 | width=8, 549 | height=6, 550 | dpi=300, 551 | din_size=None): 552 | if din_size == 'A4': 553 | width = 8.267 554 | height = 11.692 555 | self.plot += p9.theme(dpi=dpi) 556 | self.plot.save(filename=output_filename, width=width, height=height, verbose=False) 557 | 558 | 559 | save_as_pdf_pages = p9.save_as_pdf_pages 560 | -------------------------------------------------------------------------------- /pyggplot/svg_stack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## Copyright (c) 2009 Andrew D. Straw 4 | 5 | ## Permission is hereby granted, free of charge, to any person obtaining a copy 6 | ## of this software and associated documentation files (the "Software"), to deal 7 | ## in the Software without restriction, including without limitation the rights 8 | ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | ## copies of the Software, and to permit persons to whom the Software is 10 | ## furnished to do so, subject to the following conditions: 11 | 12 | ## The above copyright notice and this permission notice shall be included in 13 | ## all copies or substantial portions of the Software. 14 | 15 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | ## THE SOFTWARE. 22 | 23 | from lxml import etree # Ubuntu Karmic package: python-lxml 24 | import sys, re, os 25 | import base64 26 | from optparse import OptionParser 27 | 28 | VERSION = '0.0.1' # keep in sync with setup.py 29 | 30 | UNITS = ['pt','px','in','mm'] 31 | PT2IN = 1.0/72.0 32 | IN2PT = 72.0 33 | MM2PT = 72.0/25.4 34 | PT2PX = 1.25 35 | PX2PT = 1.0/1.25 36 | 37 | relIRI_re = re.compile(r'url\(#(.*)\)') 38 | 39 | def get_unit_attr(value): 40 | # coordinate handling from http://www.w3.org/TR/SVG11/coords.html#Units 41 | units = None # default (user) 42 | for unit_name in UNITS: 43 | if value.endswith(unit_name): 44 | units = unit_name 45 | value = value[:-len(unit_name)] 46 | break 47 | val_float = float(value) # this will fail if units str not parsed 48 | return val_float, units 49 | 50 | def convert_to_pixels( val, units): 51 | if units == 'px' or units is None: 52 | val_px = val 53 | elif units == 'pt': 54 | val_px = val*PT2PX 55 | elif units == 'in': 56 | val_px = val*IN2PT*PT2PX 57 | elif units == 'mm': 58 | val_px = val*MM2PT*PT2PX 59 | else: 60 | raise ValueError('unsupport unit conversion to pixels: %s'%units) 61 | return val_px 62 | 63 | def fix_ids( elem, prefix, level=0 ): 64 | ns = '{http://www.w3.org/2000/svg}' 65 | 66 | if isinstance(elem.tag,basestring) and elem.tag.startswith(ns): 67 | 68 | tag = elem.tag[len(ns):] 69 | 70 | if 'id' in elem.attrib: 71 | elem.attrib['id'] = prefix + elem.attrib['id'] 72 | 73 | # fix references (See http://www.w3.org/TR/SVGTiny12/linking.html#IRIReference ) 74 | 75 | for attrib in elem.attrib.keys(): 76 | value = elem.attrib.get(attrib,None) 77 | 78 | if value is not None: 79 | 80 | if attrib.startswith('{http://www.w3.org/1999/xlink}'): 81 | relIRI = False 82 | else: 83 | relIRI = True 84 | 85 | if (not relIRI) and value.startswith('#'): # local IRI, change 86 | iri = value[1:] 87 | value = '#' + prefix + iri 88 | elem.attrib[attrib] = value 89 | elif relIRI: 90 | newvalue = re.sub( relIRI_re, r'url(#'+prefix+r'\1)', value) 91 | if newvalue != value: 92 | elem.attrib[attrib] = newvalue 93 | 94 | # Do same for children 95 | 96 | for child in elem: 97 | fix_ids(child,prefix,level=level+1) 98 | 99 | def export_images( elem, filename_fmt='image%03d', start_idx=1 ): 100 | """replace inline images with files""" 101 | ns = '{http://www.w3.org/2000/svg}' 102 | href = '{http://www.w3.org/1999/xlink}href' 103 | count = 0 104 | if isinstance(elem.tag,basestring) and elem.tag.startswith(ns): 105 | tag = elem.tag[len(ns):] 106 | if tag=='image': 107 | buf = etree.tostring(elem,pretty_print=True) 108 | im_data = elem.attrib[href] 109 | exts = ['png','jpeg'] 110 | found = False 111 | for ext in exts: 112 | prefix = 'data:image/'+ext+';base64,' 113 | if im_data.startswith(prefix): 114 | data_base64 = im_data[len(prefix):] 115 | found = True 116 | break 117 | if not found: 118 | raise NotImplementedError('image found but not supported') 119 | 120 | # decode data 121 | data = base64.b64decode(data_base64) 122 | 123 | # save data 124 | idx = start_idx + count 125 | fname = filename_fmt%idx + '.' + ext 126 | if os.path.exists(fname): 127 | raise RuntimeError('File exists: %r'%fname) 128 | with open(fname,mode='w') as fd: 129 | fd.write( data ) 130 | 131 | # replace element with link 132 | elem.attrib[href] = fname 133 | count += 1 134 | 135 | # Do same for children 136 | for child in elem: 137 | count += export_images(child, filename_fmt=filename_fmt, 138 | start_idx=(start_idx+count) ) 139 | return count 140 | 141 | header_str = """ 142 | 144 | 145 | """ 146 | 147 | # ------------------------------------------------------------------ 148 | class Document(object): 149 | def __init__(self): 150 | self._layout = None 151 | def setLayout(self,layout): 152 | self._layout = layout 153 | def save(self,fileobj,debug_boxes=False,**kwargs): 154 | if self._layout is None: 155 | raise ValueError('No layout, cannot save.') 156 | accum = LayoutAccumulator(**kwargs) 157 | self._layout.render(accum,debug_boxes=debug_boxes) 158 | if isinstance(fileobj,file): 159 | fd = fileobj 160 | close = False 161 | else: 162 | fd = open(fileobj,mode='w') 163 | close = True 164 | buf = accum.tostring(pretty_print=True) 165 | 166 | fd.write(header_str) 167 | fd.write( buf ) 168 | if close: 169 | fd.close() 170 | 171 | class SVGFileBase(object): 172 | def __init__(self,fname): 173 | self._fname = fname 174 | self._root = etree.parse(fname).getroot() 175 | if self._root.tag != '{http://www.w3.org/2000/svg}svg': 176 | raise ValueError('expected file to have root element ') 177 | 178 | height, height_units = get_unit_attr(self._root.get('height')) 179 | width, width_units = get_unit_attr(self._root.get('width')) 180 | self._width_px = convert_to_pixels( width, width_units) 181 | self._height_px = convert_to_pixels( height, height_units) 182 | self._orig_width_px = self._width_px 183 | self._orig_height_px = self._height_px 184 | self._coord = None # unassigned 185 | 186 | def get_root(self): 187 | return self._root 188 | 189 | def get_size(self,min_size=None,box_align=None,level=None): 190 | return Size(self._width_px,self._height_px) 191 | 192 | def _set_size(self,size): 193 | self._width_px = size.width 194 | self._height_px = size.height 195 | 196 | def _set_coord(self,coord): 197 | self._coord = coord 198 | 199 | def export_images(self,*args,**kwargs): 200 | export_images(self._root,*args,**kwargs) 201 | 202 | class SVGFile(SVGFileBase): 203 | def __str__(self): 204 | return 'SVGFile(%s)'%repr(self._fname) 205 | 206 | class SVGFileNoLayout(SVGFileBase): 207 | def __init__(self,fname,x=0,y=0): 208 | self._x_offset = x 209 | self._y_offset = y 210 | super(SVGFileNoLayout,self).__init__(fname) 211 | 212 | def _set_coord(self,coord): 213 | self._coord = (coord[0] + self._x_offset, 214 | coord[1] + self._y_offset ) 215 | 216 | def __str__(self): 217 | return 'SVGFileNoLayout(%s)'%repr(self._fname) 218 | 219 | class LayoutAccumulator(object): 220 | def __init__(self): 221 | self._svgfiles = [] 222 | self._svgfiles_no_layout = [] 223 | self._raw_elements = [] 224 | 225 | def add_svg_file(self,svgfile): 226 | assert isinstance(svgfile,SVGFile) 227 | if svgfile in self._svgfiles: 228 | raise ValueError('cannot accumulate SVGFile instance twice') 229 | self._svgfiles.append( svgfile ) 230 | 231 | def add_svg_file_no_layout(self,svgfile): 232 | assert isinstance(svgfile,SVGFileNoLayout) 233 | if svgfile in self._svgfiles_no_layout: 234 | raise ValueError('cannot accumulate SVGFileNoLayout instance twice') 235 | self._svgfiles_no_layout.append( svgfile ) 236 | 237 | def add_raw_element(self,elem): 238 | self._raw_elements.append( elem ) 239 | 240 | def tostring(self, **kwargs): 241 | root = self._make_finalized_root() 242 | return etree.tostring(root,**kwargs) 243 | 244 | def _set_size(self,size): 245 | self._size = size 246 | 247 | def _make_finalized_root(self): 248 | # get all required namespaces and prefixes 249 | NSMAP = {None : 'http://www.w3.org/2000/svg', 250 | 'sodipodi':'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', 251 | } 252 | for svgfile in self._svgfiles: 253 | origelem = svgfile.get_root() 254 | for key,value in origelem.nsmap.iteritems(): 255 | if key in NSMAP: 256 | assert value == NSMAP[key] 257 | # Already in namespace dictionary 258 | continue 259 | elif key == 'svg': 260 | assert value == NSMAP[None] 261 | # svg is the default namespace - don't insert again. 262 | continue 263 | NSMAP[key] = value 264 | 265 | root = etree.Element('{http://www.w3.org/2000/svg}svg', 266 | nsmap=NSMAP) 267 | 268 | if 1: 269 | # inkscape hack 270 | root_defs = etree.SubElement(root,'{http://www.w3.org/2000/svg}defs') 271 | 272 | root.attrib['version']='1.1' 273 | fname_num = 0 274 | do_layout = True 275 | work_list=[] 276 | for svgfile in (self._svgfiles): 277 | work_list.append( (fname_num, do_layout, svgfile) ) 278 | fname_num += 1 279 | do_layout = False 280 | for svgfile in (self._svgfiles_no_layout): 281 | work_list.append( (fname_num, do_layout, svgfile) ) 282 | fname_num += 1 283 | for (fname_num, do_layout, svgfile) in work_list: 284 | origelem = svgfile.get_root() 285 | 286 | fix_id_prefix = 'id%d:'%fname_num 287 | elem = etree.SubElement(root,'{http://www.w3.org/2000/svg}g') 288 | elem.attrib['id'] = 'id%d'%fname_num 289 | 290 | elem_sz = svgfile.get_size() 291 | width_px = elem_sz.width 292 | height_px = elem_sz.height 293 | 294 | # copy svg contents into new group 295 | for child in origelem: 296 | if 1: 297 | # inkscape hacks 298 | if child.tag == '{http://www.w3.org/2000/svg}defs': 299 | # copy into root_defs, not into sub-group 300 | for subchild in child: 301 | fix_ids( subchild, fix_id_prefix ) 302 | root_defs.append( subchild ) 303 | continue 304 | elif child.tag == '{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}:namedview': 305 | # don't copy 306 | continue 307 | elif child.tag == '{http://www.w3.org/2000/svg}metadata': 308 | # don't copy 309 | continue 310 | elem.append(child) 311 | 312 | fix_ids( elem, fix_id_prefix ) 313 | 314 | translate_x = svgfile._coord[0] 315 | translate_y = svgfile._coord[1] 316 | if do_layout: 317 | if svgfile._orig_width_px != width_px: 318 | raise NotImplementedError('rescaling width not implemented ' 319 | '(hint: set alignment on file %s)'%( 320 | svgfile,)) 321 | if svgfile._orig_height_px != height_px: 322 | raise NotImplementedError('rescaling height not implemented ' 323 | '(hint: set alignment on file %s)'%( 324 | svgfile,)) 325 | orig_viewBox = origelem.get('viewBox') 326 | if orig_viewBox is not None: 327 | # split by comma or whitespace 328 | vb_tup = orig_viewBox.split(',') 329 | vb_tup = [c.strip() for c in vb_tup] 330 | if len(vb_tup)==1: 331 | # not separated by commas 332 | vb_tup = orig_viewBox.split() 333 | assert len(vb_tup)==4 334 | vb_tup = [float(v) for v in vb_tup] 335 | vbminx, vbminy, vbwidth, vbheight = vb_tup 336 | sx = width_px / vbwidth 337 | sy = height_px / vbheight 338 | tx = translate_x - vbminx 339 | ty = translate_y - vbminy 340 | elem.attrib['transform'] = 'matrix(%s,0,0,%s,%s,%s)'%( 341 | sx,sy,tx,ty) 342 | else: 343 | elem.attrib['transform'] = 'translate(%s,%s)'%( 344 | translate_x, translate_y) 345 | root.append( elem ) 346 | for elem in self._raw_elements: 347 | root.append(elem) 348 | 349 | root.attrib["width"] = repr(self._size.width) 350 | root.attrib["height"] = repr(self._size.height) 351 | 352 | return root 353 | 354 | # ------------------------------------------------------------------ 355 | class Size(object): 356 | def __init__(self, width=0, height=0): 357 | self.width=width 358 | self.height=height 359 | 360 | # directions for BoxLayout 361 | LeftToRight = 'LeftToRight' 362 | RightToLeft = 'RightToLeft' 363 | TopToBottom = 'TopToBottom' 364 | BottomToTop = 'BottomToTop' 365 | 366 | # alignment values 367 | AlignLeft = 0x01 368 | AlignRight = 0x02 369 | AlignHCenter = 0x04 370 | 371 | AlignTop = 0x020 372 | AlignBottom = 0x040 373 | AlignVCenter = 0x080 374 | 375 | AlignCenter = AlignHCenter | AlignVCenter 376 | 377 | class Layout(object): 378 | def __init__(self, parent=None): 379 | if parent is not None: 380 | raise NotImplementedError('') 381 | 382 | class BoxLayout(Layout): 383 | def __init__(self, direction, parent=None): 384 | super(BoxLayout,self).__init__(parent=parent) 385 | self._direction = direction 386 | self._items = [] 387 | self._contents_margins = 0 # around edge of box 388 | self._spacing = 0 # between items in box 389 | self._coord = (0,0) # default 390 | self._size = None # uncalculated 391 | 392 | def _set_coord(self,coord): 393 | self._coord = coord 394 | 395 | def render(self,accum, min_size=None, level=0, debug_boxes=0): 396 | size = self.get_size(min_size=min_size) 397 | if level==0: 398 | # set document size if top level 399 | accum._set_size(size) 400 | if debug_boxes>0: 401 | # draw black line around BoxLayout element 402 | debug_box = etree.Element('{http://www.w3.org/2000/svg}rect') 403 | debug_box.attrib['style']=( 404 | 'fill: none; stroke: black; stroke-width: 2.000000;') 405 | sz=size 406 | debug_box.attrib['x']=repr(self._coord[0]) 407 | debug_box.attrib['y']=repr(self._coord[1]) 408 | debug_box.attrib['width']=repr(sz.width) 409 | debug_box.attrib['height']=repr(sz.height) 410 | accum.add_raw_element(debug_box) 411 | 412 | for (item,stretch,alignment,xml) in self._items: 413 | if isinstance( item, SVGFile ): 414 | accum.add_svg_file(item) 415 | 416 | if debug_boxes>0: 417 | # draw red line around SVG file 418 | debug_box= etree.Element('{http://www.w3.org/2000/svg}rect') 419 | debug_box.attrib['style']=( 420 | 'fill: none; stroke: red; stroke-width: 1.000000;') 421 | sz=item.get_size() 422 | debug_box.attrib['x']=repr(item._coord[0]) 423 | debug_box.attrib['y']=repr(item._coord[1]) 424 | debug_box.attrib['width']=repr(sz.width) 425 | debug_box.attrib['height']=repr(sz.height) 426 | accum.add_raw_element(debug_box) 427 | elif isinstance( item, SVGFileNoLayout ): 428 | accum.add_svg_file_no_layout(item) 429 | 430 | if debug_boxes>0: 431 | # draw green line around SVG file 432 | debug_box= etree.Element('{http://www.w3.org/2000/svg}rect') 433 | debug_box.attrib['style']=( 434 | 'fill: none; stroke: green; stroke-width: 1.000000;') 435 | sz=item.get_size() 436 | debug_box.attrib['x']=repr(item._coord[0]) 437 | debug_box.attrib['y']=repr(item._coord[1]) 438 | debug_box.attrib['width']=repr(sz.width) 439 | debug_box.attrib['height']=repr(sz.height) 440 | accum.add_raw_element(debug_box) 441 | 442 | elif isinstance( item, BoxLayout ): 443 | item.render( accum, min_size=item._size, level=level+1, 444 | debug_boxes=debug_boxes) 445 | else: 446 | raise NotImplementedError( 447 | "don't know how to accumulate item %s"%item) 448 | 449 | if xml is not None: 450 | extra = etree.Element('{http://www.w3.org/2000/svg}g') 451 | extra.attrib['transform'] = 'translate(%s,%s)'%( 452 | repr(item._coord[0]),repr(item._coord[1])) 453 | extra.append(xml) 454 | accum.add_raw_element(extra) 455 | 456 | def get_size(self, min_size=None, box_align=0, level=0 ): 457 | cum_dim = 0 # size along layout direction 458 | max_orth_dim = 0 # size along other direction 459 | 460 | if min_size is None: 461 | min_size = Size(0,0) 462 | 463 | # Step 1: calculate required size along self._direction 464 | if self._direction in [LeftToRight, RightToLeft]: 465 | max_orth_dim = min_size.height 466 | dim_min_size = Size(width=0, 467 | height=max_orth_dim) 468 | else: 469 | max_orth_dim = min_size.width 470 | dim_min_size = Size(width=max_orth_dim, 471 | height=0) 472 | 473 | cum_dim += self._contents_margins # first margin 474 | item_sizes = [] 475 | for item_number,(item,stretch,alignment,xml) in enumerate(self._items): 476 | if isinstance(item,SVGFileNoLayout): 477 | item_size = Size(0,0) 478 | else: 479 | item_size = item.get_size(min_size=dim_min_size, box_align=alignment,level=level+1) 480 | item_sizes.append( item_size ) 481 | 482 | if isinstance(item,SVGFileNoLayout): 483 | # no layout for this file 484 | continue 485 | 486 | if self._direction in [LeftToRight, RightToLeft]: 487 | cum_dim += item_size.width 488 | max_orth_dim = max(max_orth_dim,item_size.height) 489 | else: 490 | cum_dim += item_size.height 491 | max_orth_dim = max(max_orth_dim,item_size.width) 492 | 493 | if (item_number+1) < len(self._items): 494 | cum_dim += self._spacing # space between elements 495 | cum_dim += self._contents_margins # last margin 496 | orth_dim = max_orth_dim # value without adding margins 497 | max_orth_dim += 2*self._contents_margins # margins 498 | 499 | # --------------------------------- 500 | 501 | # Step 2: another pass in which expansion takes place 502 | total_stretch = 0 503 | for item,stretch,alignment,xml in self._items: 504 | total_stretch += stretch 505 | if (self._direction in [LeftToRight, RightToLeft]): 506 | dim_unfilled_length = max(0,min_size.width - cum_dim) 507 | else: 508 | dim_unfilled_length = max(0,min_size.height - cum_dim) 509 | 510 | stretch_hack = False 511 | if dim_unfilled_length > 0: 512 | if total_stretch == 0: 513 | # BoxLayout in which stretch is 0, but unfilled space 514 | # exists. 515 | 516 | # XXX TODO: what is Qt policy in this case? 517 | stretch_hack = True 518 | stretch_inc = 0 519 | else: 520 | stretch_inc = dim_unfilled_length / float(total_stretch) 521 | else: 522 | stretch_inc = 0 523 | 524 | cum_dim = 0 # size along layout direction 525 | cum_dim += self._contents_margins # first margin 526 | is_last_item = False 527 | for i,(_item,old_item_size) in enumerate(zip(self._items,item_sizes)): 528 | if (i+1) >= len(self._items): 529 | is_last_item=True 530 | (item,stretch,alignment,xml) = _item 531 | if (self._direction in [LeftToRight, RightToLeft]): 532 | new_dim_length = old_item_size.width + stretch*stretch_inc 533 | if stretch_hack and is_last_item: 534 | new_dim_length = old_item_size.width + dim_unfilled_length 535 | new_item_size = Size( new_dim_length, orth_dim ) 536 | else: 537 | new_dim_length = old_item_size.height + stretch*stretch_inc 538 | if stretch_hack and is_last_item: 539 | new_dim_length = old_item_size.width + dim_unfilled_length 540 | new_item_size = Size( orth_dim, new_dim_length ) 541 | 542 | if isinstance(item,SVGFileNoLayout): 543 | item_size = Size(0,0) 544 | else: 545 | item_size = item.get_size(min_size=new_item_size, box_align=alignment,level=level+1) 546 | if self._direction == LeftToRight: 547 | child_box_coord = (cum_dim, self._contents_margins) 548 | elif self._direction == TopToBottom: 549 | child_box_coord = (self._contents_margins, cum_dim) 550 | else: 551 | raise NotImplementedError( 552 | 'direction %s not implemented'%self._direction) 553 | child_box_coord = (child_box_coord[0] + self._coord[0], 554 | child_box_coord[1] + self._coord[1]) 555 | child_box_size = new_item_size 556 | 557 | item_pos, final_item_size = self._calc_box( child_box_coord, child_box_size, 558 | item_size, 559 | alignment ) 560 | item._set_coord( item_pos ) 561 | item._set_size( final_item_size ) 562 | 563 | if self._direction in [LeftToRight, RightToLeft]: 564 | # Use requested item size so ill behaved item doesn't 565 | # screw up layout. 566 | cum_dim += new_item_size.width 567 | else: 568 | # Use requested item size so ill behaved item doesn't 569 | # screw up layout. 570 | cum_dim += new_item_size.height 571 | 572 | if not is_last_item: 573 | cum_dim += self._spacing # space between elements 574 | cum_dim += self._contents_margins # last margin 575 | 576 | # --------------------------------- 577 | 578 | # Step 3: calculate coordinates of each item 579 | 580 | if self._direction in [LeftToRight, RightToLeft]: 581 | size = Size(cum_dim, max_orth_dim) 582 | else: 583 | size = Size(max_orth_dim, cum_dim) 584 | 585 | self._size = size 586 | return size 587 | 588 | def _calc_box(self, in_pos, in_sz, item_sz, alignment): 589 | if (AlignLeft & alignment): 590 | left = in_pos[0] 591 | width = item_sz.width 592 | elif (AlignRight & alignment): 593 | left = in_pos[0]+in_sz.width-item_sz.width 594 | width = item_sz.width 595 | elif (AlignHCenter & alignment): 596 | left = in_pos[0]+0.5*(in_sz.width-item_sz.width) 597 | width = item_sz.width 598 | else: 599 | # expand 600 | left = in_pos[0] 601 | width = in_sz.width 602 | 603 | if (AlignTop & alignment): 604 | top = in_pos[1] 605 | height = item_sz.height 606 | elif (AlignBottom & alignment): 607 | top = in_pos[1]+in_sz.height-item_sz.height 608 | height = item_sz.height 609 | elif (AlignVCenter & alignment): 610 | top = in_pos[1]+0.5*(in_sz.height-item_sz.height) 611 | height = item_sz.height 612 | else: 613 | # expand 614 | top = in_pos[1] 615 | height = in_sz.height 616 | 617 | pos = (left,top) 618 | size = Size(width,height) 619 | return pos,size 620 | 621 | def _set_size(self, size): 622 | self._size = size 623 | 624 | def setSpacing(self,spacing): 625 | self._spacing = spacing 626 | 627 | def addSVG(self, svg_file, stretch=0, alignment=0, xml=None): 628 | if not isinstance(svg_file,SVGFile): 629 | svg_file = SVGFile(svg_file) 630 | if xml is not None: 631 | xml = etree.XML(xml) 632 | self._items.append((svg_file,stretch,alignment,xml)) 633 | 634 | def addSVGNoLayout(self, svg_file, x=0, y=0, xml=None): 635 | if not isinstance(svg_file,SVGFileNoLayout): 636 | svg_file = SVGFileNoLayout(svg_file,x=x,y=y) 637 | stretch=0 638 | alignment=0 639 | if xml is not None: 640 | xml = etree.XML(xml) 641 | self._items.append((svg_file,stretch,alignment,xml)) 642 | 643 | def addLayout(self, layout, stretch=0): 644 | assert isinstance(layout,Layout) 645 | alignment=0 # always expand a layout 646 | xml=None 647 | self._items.append((layout,stretch,alignment,xml)) 648 | 649 | class HBoxLayout(BoxLayout): 650 | def __init__(self, parent=None): 651 | super(HBoxLayout,self).__init__(LeftToRight,parent=parent) 652 | 653 | class VBoxLayout(BoxLayout): 654 | def __init__(self, parent=None): 655 | super(VBoxLayout,self).__init__(TopToBottom,parent=parent) 656 | 657 | # ------------------------------------------------------------------ 658 | 659 | def main(): 660 | usage = '''%prog FILE1 [FILE2] [...] [options] 661 | 662 | concatenate SVG files 663 | 664 | This will concatenate FILE1, FILE2, ... to a new svg file printed to 665 | stdout. 666 | 667 | ''' 668 | 669 | parser = OptionParser(usage, version=VERSION) 670 | parser.add_option("--margin",type='str', 671 | help='size of margin (in any units, px default)', 672 | default=None) 673 | parser.add_option("--direction",type='str', 674 | default='vertical', 675 | help='horizontal or vertical (or h or v)') 676 | (options, args) = parser.parse_args() 677 | fnames = args 678 | 679 | if options.direction.lower().startswith('v'): 680 | direction = 'vertical' 681 | elif options.direction.lower().startswith('h'): 682 | direction = 'horizontal' 683 | else: 684 | raise ValueError('unknown direction %s'%options.direction) 685 | 686 | if options.margin is not None: 687 | margin_px = convert_to_pixels(*get_unit_attr(options.margin)) 688 | else: 689 | margin_px = 0 690 | 691 | if 0: 692 | fd = open('tmp.svg',mode='w') 693 | else: 694 | fd = sys.stdout 695 | 696 | doc = Document() 697 | if direction == 'vertical': 698 | layout = VBoxLayout() 699 | elif direction == 'horizontal': 700 | layout = HBoxLayout() 701 | 702 | for fname in fnames: 703 | layout.addSVG(fname,alignment=AlignCenter) 704 | 705 | layout.setSpacing(margin_px) 706 | doc.setLayout(layout) 707 | doc.save( fd ) 708 | 709 | if __name__=='__main__': 710 | main() 711 | -------------------------------------------------------------------------------- /register.py: -------------------------------------------------------------------------------- 1 | import pandoc 2 | import os 3 | 4 | pandoc.core.PANDOC_PATH = '/usr/bin/pandoc' 5 | 6 | doc = pandoc.Document() 7 | doc.markdown = open('README.md').read() 8 | f = open('README.txt','w+') 9 | f.write(doc.rst) 10 | f.close() 11 | os.system("python setup.py sdist upload") 12 | os.remove('README.txt') 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup, find_packages 3 | except: 4 | from distutils.core import setup 5 | import os 6 | 7 | long_description = "Please visit https://github.com/TyberiusPrime/pyggplot for full description" 8 | if os.path.exists('README.md'): 9 | with open('README.md') as op: 10 | long_description = op.read() 11 | 12 | setup( 13 | name='pyggplot', 14 | version='27', 15 | packages=['pyggplot',], 16 | license='BSD', 17 | url='https://github.com/TyberiusPrime/pyggplot', 18 | author='Florian Finkernagel', 19 | description = "A Pythonic wrapper around R's ggplot", 20 | author_email='finkernagel@coonabibba.de', 21 | long_description=long_description, 22 | package_data={'pyggplot': ['LICENSE.txt', 'README.md']}, 23 | include_package_data=True, 24 | install_requires=[ 25 | 'pandas>=0.15', 26 | 'plotnine', 27 | 'ordereddict', 28 | ], 29 | classifiers=['Development Status :: 3 - Alpha', 30 | 'Intended Audience :: Science/Research', 31 | 'Topic :: Scientific/Engineering', 32 | 'Topic :: Scientific/Engineering :: Visualization', 33 | 'Operating System :: Microsoft :: Windows', 34 | 'Operating System :: Unix', 35 | 'Operating System :: MacOS', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.4'], 41 | ) 42 | --------------------------------------------------------------------------------